公告

👇微信扫码添加好友👇

Skip to content

每日一题 - 202405

5-31

如何压缩前端项目中 JS 的体积

  1. terser 或者 uglify,及流行的使用 Rust 编写的 swc 压缩混淆化 JS。
  2. gzip 或者 brotli 压缩,在网关处(nginx)开启
  3. 使用 webpack-bundle-analyzer 分析打包体积,替换占用较大体积的库,如 moment -> dayjs
  4. 使用支持 Tree-Shaking 的库,对无引用的库或函数进行删除,如 lodash -> lodash/es
  5. 对无法 Tree Shaking 的库,进行按需引入模块,如使用 import Button from 'antd/lib/Button',此处可手写 babel-plugin 自动完成,但不推荐
  6. 使用 babel (css 为 postcss) 时采用 browserlist,越先进的浏览器所需要的 polyfill 越少,体积更小
  7. code spliting,路由懒加载,只加载当前路由的包,按需加载其余的 chunk,首页 JS 体积变小 (PS: 次条不减小总体积,但减小首页体积)
  8. 使用 webpack 的 splitChunksPlugin,把运行时、被引用多次的库进行分包,在分包时要注意避免某一个库被多次引用多次打包。此时分为多个 chunk,虽不能把总体积变小,但可提高加载性能 (PS: 此条不减小总体积,但可提升加载性能)
  9. 去除多余字符,eg:空格,换行、注释
  10. 使用更简单的表达,eg:合并声明、布尔值简化

5-30

prefetch 与 preload 的区别是什么

preload 提供了一种声明式的命令,让浏览器提前加载指定资源(加载后并不执行),在需要执行的时候再执行。提供的好处主要是

  • 将加载和执行分离开,可不阻塞渲染和 document 的 onload 事件
  • 提前加载指定资源,不再出现依赖的font字体隔了一段时间才刷出
html
<link rel="prefetch" href="style.css" as="style" />
<link rel="preload" href="main.js" as="script" />
  1. preload 优先级高,是告诉浏览器页面必定需要的资源,浏览器一定会加载这些资源。一般对于 Bundle Spliting 资源与 Code Spliting 资源做 preload
  2. prefetch 优先级低,是告诉浏览器页面可能需要的资源,浏览器不一定会加载这些资源。一般用以加载其它路由资源,如当页面出现 Link,可 prefetch 当前 Link 的路由资源。(next.js 默认会对 link 做懒加载+prefetch,即当某条 Link 出现页面中,即自动 prefetch 该 Link 指向的路由资源

若不确定资源是必定会加载的,则不要错误使用 preload,以免本末倒置,给页面带来更沉重的负担。

当然,可以在 PC 中使用 preload 来刷新资源的缓存,但在移动端则需要特别慎重,因为可能会浪费用户的带宽。

preload 和 prefetch 混用的话,并不会复用资源,而是会重复加载。

若 css 中有应用于已渲染到 DOM 树的元素的选择器,且设置了@font-face规则时,会触发字体文件的加载。 而字体文件加载中时,DOM 中的这些元素,是处于不可见的状态。对已知必加载的 font 文件进行预加载,除了有性能提升外,更有体验优化的效果。

5-29

简述 node/v8 中的垃圾回收机制

v8 中的垃圾回收机制分为三种

  1. Scavenge,工作在新生代,把 from space 中的存活对象移至 to space
  2. Mark-Sweep,标记清除。新生代的某些对象由于过度活跃会被移至老生代,此时对老生代中活对象进行标记,并清理死对象
  3. Mark-Compact,标记整理。

当一个函数执行结束之后,JavaScript 引擎会通过向下移动 ESP 来销毁该函数保存在栈中的执行上下文。 要回收堆中的垃圾数据,就需要用到 JavaScript 中的垃圾回收器了。 代际假说(The Generational Hypothesis),是垃圾回收领域中一个重要的术语,后续垃圾回收的策略都是建立在该假说的基础之上的,所以很是重要。 代际假说有以下两个特点:

  1. 大部分对象在内存中存在的时间很短,简单来说,就是很多对象一经分配内存,很快就变得不可访问;

  2. 是不死的对象,会活得更久。 在 V8 中会把堆分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象。

    ● 副垃圾回收器,主要负责新生代的垃圾回收。

    ● 主垃圾回收器,主要负责老生代的垃圾回收。

全停顿1620974853612-a480b43e-b3bb-452f-a502-3a0162548a7f

增量标记1620975019709-efc33748-fbfc-4fb5-a19d-97abadbf8f97

使用增量标记算法,可以把一个完整的垃圾回收任务拆分为很多小的任务,这些小的任务执行时间比较短,可以穿插在其他的 JavaScript 任务中间执行,增强用户体验。

5-28

JS如何在url中传递数组

在 URL 中如何传递数组这种复杂的数据,完全取决于项目中前后端成员关于复杂数据在 URL 中传输的约定,一般情况下可以使用以下方式来传递数组:

javascript
a = 3 & a = 4 & a = 5;

a = 3, 4, 5;

a[] = 3 & a[] = 4 & a[] = 5;

a[0] = 3 & a[1] = 4 & a[2] = 5;

但同样,需要后端开发者写一个 querystring.parse 来对指定的格式解析进行支持,同时也有对各种复杂 qs 支持较好的 package,如:qs: 据说是对 querystring 复杂对象解析最好的库

5-27

Vue3中的ref、toRef和toRefs

ref:接收一个内部值,生成对应的响应式数据,该内部值挂载在ref对象的value属性上;该对象可以用于模版和reactive。使用ref是为了解决值类型在setup、computed、合成函数等情况下的响应式丢失问题。

toRef:为响应式对象(reactive)的一个属性创建对应的ref,且该方式创建的ref与源属性保持同步。

toRefs:将响应式对象转换成普通对象,对象的每个属性都是对应的ref,两者间保持同步。使用toRefs进行对象解构。

javascript
function ref(val) {
    const wrapper = {value: val}
    Object.defineProperty(wrapper, '__v_isRef', {value: true})
    return reactive(wrapper)
}

function toRef(obj, key) {
    const wrapper = {
        get value() {
            return obj[key]
        },
        set value(val) {
            obj[key] = val
        }
    }
    Object.defineProperty(wrapper, '__v_isRef', {value: true})
    return wrapper
}

function toRefs(obj) {
    const ret = {}
    for (const key in obj) {
        ret[key] = toRef(obj, key)
    }
    
    return ret
}

// 自动脱ref
function proxyRefs(target) {
    return new Proxy(target, {
        get(target, key, receiver) {
            const value = Reflect.get(target, key, receiver)
            return value.__v_isRef ? value.value : value
        },
        set(target, key, newValue, receiver) {
            const value = target[key]
            if(value.__v_isRef) {
                value.value = newValue
                return true
            }
            return Reflect.set(target, key, newValue, receiver)
        }
    })
}

5-26

TypeScript中any、unknown、never

any和unkonwn在TS类型中属于最顶层的Top Type,即所有的类型都是它俩的子类型。

never则相反,它作为Bottom Type是所有类型的子类型。

5-25

浏览器的事件循环机制

JavaScript是单线程的(指的是js引擎在执行代码的时候只有一个主线程,每次只能干一件事),同时还是非阻塞运行的(执行异步任务的时候,会先挂起相应任务,待异步返回结果再执行回调)

在js代码执行时,会将对象存在堆(heap)中,在栈(stack)中存放一些基础类型变量和对象的指针。在执行方法时,会根据当前方法的执行上下文,来进行一个执行。对于普通函数就是正常的入栈出栈即可,涉及到异步任务的时候,js执行会将对应的任务放到事件队列中(微任务队列、宏任务队列)。

  • 常见微任务:queueMicrotask、Promise、MutationObserve等。
  • 常见宏任务:ajax、setTimeout、setInterval、script(js整体代码)、IO操作、UI交互、postMessage等。

故事件循环可以理解为是一个桥梁,连接着应用程序的js和系统调用之间的通道。其过程为:

  1. 执行一个宏任务(一般为一段script),若没有可选的宏任务,就直接处理微任务。
  2. 执行中遇到微任务,就将其添加到微任务的任务队列中。
  3. 执行中遇到宏任务,就将其提交到宏任务队列中。
  4. 执行完当前执行的宏任务后,去查询当前有无需要执行的微任务,有就执行
  5. 检查渲染,若需要渲染,浏览器执行渲染任务
  6. 渲染完毕后,Js线程会去执行下一个宏任务。。。(如此循环)
javascript
console.log("script start");

const promiseA = new Promise((resolve, reject) => {
  console.log("init promiseA");
  resolve("promiseA");
});

const promiseB = new Promise((resolve, reject) => {
  console.log("init promiseB");
  resolve("promiseB");
});

setTimeout(() => {
  console.log("setTimeout run");
  promiseB.then(res => {
    console.log("promiseB res :>> ", res);
  });
  console.log("setTimeout end");
}, 500);

promiseA.then(res => {
  console.log("promiseA res :>> ", res);
});

queueMicrotask(() => {
  console.log("queue Microtask run");
});

console.log("script end");

// script start
// init promiseA
// init promiseB
// script end
// promiseA res :>>  promiseA
// queue Microtask run
// setTimeout run
// setTimeout end
// promiseB res :>>  promiseB

5-24

箭头函数和普通函数的区别

箭头函数不会创建自身的this,只会从上一级继承this,箭头函数的this在定义的时候就已经确认了,之后不会改变。同时箭头函数无法作为构造函数使用,没有自身的prototype,也没有arguments

javascript
this.id = "global";

console.log("this.id :>> ", this.id); // this.id :>>  global

function normalFun() {
  return this.id;
}

const arrowFun = () => {
  return this.id;
};

const newNormal = new normalFun();
console.log("newNormal :>> ", newNormal); // newNormal :>>  normalFun {}
try {
  const newArrow = new arrowFun();
} catch (error) {
  console.log("error :>> ", error); // error :>>  TypeError: arrowFun is not a constructor
}

console.log("normalFun :>> ", normalFun()); // normalFun :>>  undefined
console.log("arrowFun() :>> ", arrowFun()); // arrowFun() :>>  global

const obj = {
  id: "obj",
  normalFun,
  arrowFun,
};

const normalFunBindObj = normalFun.bind(obj);
const arrowFunBindObj = arrowFun.bind(obj);
console.log("normalFun.call(obj) :>> ", normalFun.call(obj)); // normalFun.call(obj) :>>  obj
console.log("normalFunBindObj() :>> ", normalFunBindObj()); // normalFunBindObj() :>>  obj
console.log("arrowFun.call(obj) :>> :>> ", arrowFun.call(obj)); // arrowFun.call(obj) :>> :>>  global
console.log("arrowFunBindObj() :>> ", arrowFunBindObj()); // arrowFunBindObj() :>>  global
console.log("obj.normalFun() :>> ", obj.normalFun()); // obj.normalFun() :>>  obj
console.log("obj.arrowFun() :>> ", obj.arrowFun()); // obj.arrowFun() :>>  global

5-23

实现一个类似关键字new功能的函数

在js中new关键字主要做了:首先创建一个空对象,这个对象会作为执行new构造函数之后返回的对象实例,将创建的空对象原型(__proto__)指向构造函数的prototype属性,同时将这个空对象赋值给构造函数内部的this,并执行构造函数逻辑,根据构造函数的执行逻辑,返回初始创建的对象或构造函数的显式返回值。

javascript
function newFn(...args) {
  const constructor = args.shift();
  const obj = Object.create(constructor.prototype);
  const result = constructor.apply(obj, args);
  return typeof result === "object" && result !== null ? result : obj;
}

function Person(name) {
  this.name = name;
}

const p = newFn(Person, "Jerome");

console.log("p.name :>> ", p.name); // p.name :>>  Jerome

5-22

数组的forEach和map方法的区别

forEach是对数组的每一个元素执行一次给定的函数。

map是创建一个新数组,该新数组由原数组的每个元素都调用一次提供的函数返回值。

javascript
const arr = [1,2,3,4,5,6];

arr.forEach(x =>{
    x = x + 1;
    console.log("x :>> ", x);
})
// x :>>  2
// x :>>  3
// x :>>  4
// x :>>  5
// x :>>  6
// x :>>  7
console.log("arr :>> ", arr); // arr :>>  [1,2,3,4,5,6]

const mapArr = arr.map(x =>{
    x = x * 2;
    return x;
})
console.log("mapArr :>> ", mapArr); // mapArr :>>  [2,4,6,8,10,12]
console.log("arr :>> ", arr);  // arr :>>  [1,2,3,4,5,6]

pop():删除数组后面的最后一个元素,返回值为被删除的那个元素。

push():将一个元素或多个元素添加到数组末尾,并返回新的长度。

shift():删除数组中的第一个元素,并返回被删除元素的值。

unshift():将一个或多个元素添加到数组的开头,并返回该数组的新长度

splice():通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。

reverse(): 反转数组。

5-21

TS中type和interface的区别

interface可以重复声明,type不行,继承方式不一样,type使用交叉类型方式,interface使用extends实现。在对象扩展的情况下,使用接口继承要比交叉类型的性能更好。建议使用interface来描述对象对外暴露的借口,使用type将一组类型重命名(或对类型进行复杂编程)。

typescript
interface iMan {
  name: string;
  age: number;
}
// 接口可以进行声明合并
interface iMan {
  hobby: string;
}

type tMan = {
  name: string;
  age: number;
};
// type不能重复定义
// type tMan = {}

// 继承方式不同,接口继承使用extends
interface iManPlus extends iMan {
  height: string;
}
// type继承使用&,又称交叉类型
type tManPlus = { height: string } & tMan;

const aMan: iManPlus = {
  name: "aa",
  age: 15,
  height: "175cm",
  hobby: "eat",
};

const bMan: tManPlus = {
  name: "bb",
  age: 15,
  height: "150cm",
};

5-20

理解provide与inject

1、provide和inject是一对新的API,用于在父组件中提供数据,然后在子组件中注入数据。

2、provide:是一个对象,或者是一个返回对象的函数。里面呢就包含要给子孙后代的东西,也就是属性和属性值。

3、inject:一个字符串数组,或者是一个对象。属性值可以是一个对象,包含from和default默认值。

vue
//在父组件中,使用provide提供数据:
//name:定义提供 property的 name。
//value :property的值。
 setup(){
    provide('info',"值")
  }
//在子组件中,使用inject注入数据
//name:接收 provide提供的属性名。
//default:设置默认值,可以不写,是可选参数。
setup(){
    const info = inject("info")
    inject('info',"设置默认值")
    return {
        info
    }
  }

provide和inject只能在setup函数中使用,而且provide提供的数据只能在其子组件中使用。如果要在兄弟组件中共享数据,可以使用一个共享的对象或者使用Vuex等状态管理库。

5-19

js-tool-big-box工具包

js-tool-big-box工具主要解决防抖(debounce)和节流(throttle)的公共方法

防抖

javascript
<template>
  <div>
    <input @keyup="handleChange" v-model="inputVal" />
  </div>
</template>
<script>
import { eventBox } from 'js-tool-big-box';
export default {
  data() {
    return {
      inputVal: ''
    }
  },
    created() {
        this.myDebounce = eventBox.debounce((data) => {
          this.sendAjax(data);
        }, 2000);
      },
methods: {
    handleChange(event) {
      const val = event.target.value;
      this.myDebounce(val);
    },
    sendAjax(data) {
      console.log('发送时间::', new Date().getTime());
      console.log('发送请求:', data);
    },
  }
}
</script>

节流

javascript
<script>
import { eventBox } from 'js-tool-big-box';
 
export default {
  name: 'dj',
  data () {
    return {
      inputVal: ''
    }
  },
  created() {
    this.myThrottle = eventBox.throttle((data) => {
      this.sendAjax(data);
    }, 2000);
  },
  methods: {
    handleChange(event) {
      const val = event.target.value;
      this.myThrottle(val);
    },
    sendAjax(data) {
      console.log('发送时间::', new Date().getTime());
      console.log('发送请求:', data);
    },
  }
}
</script>

5-18

Flutter状态管理

以下是常用的状态管理框架:

1、state状态管理

dart
 @override
  InheritedWidget inheritFromWidgetOfExactType(Type targetType, { Object aspect }) {
    /// 在共享 map _inheritedWidgets 中查找
    final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
    if (ancestor != null) {
      /// 返回找到的 InheritedWidget ,同时添加当前 element 处理
      return inheritFromElement(ancestor, aspect: aspect);
    }
    _hadUnsatisfiedDependencies = true;
    return null;
  }

  @override
  InheritedWidget inheritFromElement(InheritedElement ancestor, { Object aspect }) {
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies.add(ancestor);
   /// 就是将当前 element(this) 添加到  _dependents 里
   /// 也就是 InheritedElement 的 _dependents
   /// _dependents[dependent] = value;
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
  }

  @override
  void notifyClients(InheritedWidget oldWidget) {
    for (Element dependent in _dependents.keys) {
      notifyDependent(oldWidget, dependent);
    }
  }

2、Provider状态管理

优点: 不复杂,好理解,代码量不大的情况下,可以方便组合和控制刷新颗粒度 , 其实一开始官方也有一个 flutter-provide ,不过后来无了, Provider成了它的替代品。

缺点:相对依赖Flutter 和 Widget;需要依赖Context

dart
class _ProviderPageState extends State<ProviderPage> {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(builder: (_) => ProviderModel()),
      ],
      child: Scaffold(
        appBar: AppBar(
          title: LayoutBuilder(
            builder: (BuildContext context, BoxConstraints constraints) {
              var counter =  Provider.of<ProviderModel>(context);
              return new Text("Provider ${counter.count.toString()}");
            },
          )
        ),
        body: CountWidget(),
      ),
    );
  }
}

class CountWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<ProviderModel>(builder: (context, counter, _) {
      return new Column(
        children: <Widget>[
          new Expanded(child: new Center(child: new Text(counter.count.toString()))),
          new Center(
            child: new FlatButton(
                onPressed: () {
                  counter.add();
                },
                color: Colors.blue,
                child: new Text("+")),
          )
        ],
      );
    });
  }
}

class ProviderModel extends ChangeNotifier {
  int _count = 0;

  int get count => _count;

  void add() {
    _count++;
    notifyListeners();
  }
}

3、Bloc状态管理

BLoC算是 Flutter 早期比较知名的状态管理框架,它同样是存在 blocflutter_bloc 这样的依赖关系,它是基于事件驱动来实现的状态管理

优点:代码更加解耦,这是事件驱动的特性,把状态更新和事件绑定,可以灵活得实现状态拦截,重试甚至撤回

缺点:需要写更多的代码,开发节奏会有点影响,接收代码的新维护人员,缺乏有效文档时容易陷入对着事件和业务蒙圈,项目后期事件容易混乱交织

dart
BlocSelector<BlocA, BlocAState, SelectedState>(
  selector: (state) {
    // return selected state based on the provided state.
  },
  builder: (context, state) {
    // return widget here based on the selected state.
  },
)

MultiBlocListener(
  listeners: [
    BlocListener<BlocA, BlocAState>(
      listener: (context, state) {},
    ),
    BlocListener<BlocB, BlocBState>(
      listener: (context, state) {},
    ),
    BlocListener<BlocC, BlocCState>(
      listener: (context, state) {},
    ),
  ],
  child: ChildA(),
)

4、flutter_redux状态管理

优点:解耦,对 redux 开发友好,适合中大型项目里协作开发

缺点:影响开发速度,要写一堆模版,不是很贴合 Flutter 开发思路

5、GetX状态管理

优点:瑞士军刀式护航;对新人友好;可以减少很多代码

缺点:全家桶,做的太多对于一些使用者来说是致命缺点,需要解决的 Bug 也多;“魔法”使用较多,脱离 Flutter 原本轨迹;入侵性极强

5-17

了解Flutter

Flutter是由Google推出的开源UI软件开发工具包,用于构建原生、精美的移动、web和桌面应用。它使用Dart语言作为开发语言,并通过自己的渲染引擎绘制UI。

Flutter的优势:

  • 提高开发效率
    • 同一份代码开发iOS和Android
    • 用更少的代码做更多的事情
    • 轻松迭代
      • 在应用程序运行时更改代码并重新加载(通过热重载)
      • 修复崩溃并继续从应用程序停止的地方进行调试
  • 创建美观,高度定制的用户体验
    • 受益于使用Flutter框架提供的丰富的Material Design和Cupertino(iOS风格)的widget
    • 实现定制、美观、品牌驱动的设计,而不受原生控件的限制

5-16

理解keep-alive

keep-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,避免重新渲染 ,其有以下特性:

  • 一般结合路由和动态组件一起使用,用于缓存组件;
  • 提供 include 和 exclude 属性,两者都支持字符串或正则表达式, include 表示只有名称匹配的组件会被缓存,exclude 表示任何名称匹配的组件都不会被缓存 ,其中 exclude 的优先级比 include 高;
  • 对应两个钩子函数 activated 和 deactivated ,当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated。

5-15

Vue 的父组件和子组件生命周期钩子函数执行顺序

Vue 的父组件和子组件生命周期钩子函数执行顺序可以归类为以下 4 部分:

  • 加载渲染过程

    父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted

  • 子组件更新过程

    父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated

  • 父组件更新过程

    父 beforeUpdate -> 父 updated

  • 销毁过程

    父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed

5-14

Vue 生命周期

Vue 实例有一个完整的生命周期,也就是从开始创建、初始化数据、编译模版、挂载 Dom -> 渲染、更新 -> 渲染、卸载等一系列过程,我们称这是 Vue 的生命周期。

生命周期描述
beforeCreate组件实例被创建之初,组件的属性生效之前
created组件实例已经完全创建,属性也绑定,但真实 dom 还没有生成,$el 还不可用
beforeMount在挂载开始之前被调用:相关的 render 函数首次被调用
mountedel 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子
beforeUpdate组件数据更新之前调用,发生在虚拟 DOM 打补丁之前
update组件数据更新之后
activitedkeep-alive 专属,组件被激活时调用
deactivatedkeep-alive 专属,组件被销毁时调用
beforeDestory组件销毁前调用
destoryed组件销毁后调用

1.png

5-13

JS 双色球机选一注

描述

双色球由33个红球和16个蓝球组成,1注双色球包括6个不重复的红球和1个蓝球。 请阅读给出的页面和代码,完成 randomFn 函数,实现“随机一注”功能,要求如下:

函数返回:

`1.以字符串形式输出“随机一注”结果,选中的红蓝球用"|"隔开,红球在前,号码间用半角逗号隔开,如"06,10,13,18,23,27|05"

2.红球和蓝球号码排列顺序 需与页面展示的顺序对应`

页面交互:

1.将选中的红球和蓝球(页面中对应DOM元素)用class="active"高亮

2.将选中的球按号码从小到大排列,移至所属组的前方,结果如示意图所示

3.每次执行 randomFn 函数,输出符合要求且不完全重复

img

注意:

1、请使用原生JavaScript操作DOM元素,不要增加、删除DOM元素或修改css

2、请使用ES5语法

3、答题时不要使用第三方插件

4、运行浏览器为chrome浏览器

html
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
</head>

<body>
    <!-- 填写标签 -->
    <div class="main">
    <div class="balls red">
        <span>红球</span>
        <div class="balls-wp">
            <b>01</b>
            <b>02</b>
            <b>03</b>
            <b>04</b>
            <b>05</b>
            <b>06</b>
            <b>07</b>
            <b>08</b>
            <b>09</b>
            <b>10</b>
            <b>11</b>
            <b>12</b>
            <b>13</b>
            <b>14</b>
            <b>15</b>
            <b>16</b>
            <b>17</b>
            <b>18</b>
            <b>19</b>
            <b>20</b>
            <b>21</b>
            <b>22</b>
            <b>23</b>
            <b>24</b>
            <b>25</b>
            <b>26</b>
            <b>27</b>
            <b>28</b>
            <b>29</b>
            <b>30</b>
            <b>31</b>
            <b>32</b>
            <b>33</b>
        </div>
    </div>
    <div class="balls blue">
        <span>蓝球</span>
        <div class="balls-wp">
            <b>01</b>
            <b>02</b>
            <b>03</b>
            <b>04</b>
            <b>05</b>
            <b>06</b>
            <b>07</b>
            <b>08</b>
            <b>09</b>
            <b>10</b>
            <b>11</b>
            <b>12</b>
            <b>13</b>
            <b>14</b>
            <b>15</b>
            <b>16</b>
        </div>
    </div>
</div>
    <script type="text/javascript">
        // 填写JavaScript
        randomFn();

        function randomFn() {
	        let redballs = document.querySelectorAll(".red .balls-wp b")
            let blueballs = document.querySelectorAll(".blue .balls-wp b")
            let reddiv = document.querySelector(".red .balls-wp")
            let bluediv = document.querySelector(".blue .balls-wp")
            let red = []
            let redB = []
            while(red.length<6) {
                let num=Math.floor(Math.random()*33)+1
                if(!red.includes(num)) {
                    red.push(num)
                    let redb = redballs[num-1]
                    redb.classList.add("active")
                    redB.push(redb)
                }
            }
            let blue = Math.floor(Math.random()*16)+1
            let blueB = blueballs[blue-1]
            //不能直接接在后面写 因为add返回的是undefined
            blueB.classList.add("active")
            //按照从大到小 指的是b标签的内部
            redB = redB.sort((a,b)=>b.innerHTML-a.innerHTML)
            for(let i=0;i<6;i++) {
                redballs = document.querySelectorAll('.red .balls-wp b')
                //insertBefore是如果原来是子节点则将子节点移动到对应节点前面(删除原本位置)
                //反之如果原来不是子节点 则将新节点插入到对应节点前面
                reddiv.insertBefore(redB[i],redballs[0])
            }
            bluediv.insertBefore(blueB,blueballs[0])
            //因为涉及到06这种 而redb只是纯数值而已
            return redB.map(ball => ball.innerHTML).reverse().join(',') + '|' + blueB.innerHTML
        }
    </script>
</body>

</html>

5-12

JS 购物车

HTML模块为一个简化版的购物车,tbody为商品列表,tfoot为统计信息,系统会随机在列表中生成一些初始商品信息

1、请完成add函数,在列表后面显示items商品信息。参数items为{name: String, price: Number}组成的数组

2、请完成bind函数,点击每一行的删除按钮(包括通过add增加的行),从列表中删除对应行

3、请注意同步更新统计信息,价格保留小数点后两位

4、列表和统计信息格式请与HTML示例保持一致

5、不要直接手动修改HTML中的代码

6、不要使用第三方库

html
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
</head>

<body>
    <!-- 填写标签 -->
    <table id="jsTrolley">
    <thead><tr><th>名称</th><th>价格</th><th>操作</th></tr></thead>
    <tbody>
        <tr><td>产品1</td><td>10.00</td><td><a href="javascript:void(0);">删除</a></td></tr>
        <tr><td>产品2</td><td>30.20</td><td><a href="javascript:void(0);">删除</a></td></tr>
        <tr><td>产品3</td><td>20.50</td><td><a href="javascript:void(0);">删除</a></td></tr>
    </tbody>
    <tfoot><tr><th>总计</th><td colspan="2">60.70(3件商品)</td></tr></tfoot>
</table>
    <script type="text/javascript">
        // 填写JavaScript
        // 新增行
        function add(items) {
            var tbody = document.getElementsByTagName('tbody')[0]
            var tfoot = document.getElementsByTagName('tfoot')[0]
            // 获取初始数据
            let count = tbody.children.length
            let price = parseFloat(tfoot.innerText.match(/\d+.\d+/)[0])
            // 新增行
            let tr = ''
            for (let i = 0; i < items.length; i++) {
                count += 1
                price += items[i].price
                tr += `<tr><td>${items[i].name}</td><td>${items[i].price.toFixed(2)}</td><td><a href="javascript:void(0);">删除</a></td></tr>`
            }
            tbody.innerHTML += tr
            tfoot.innerHTML = `<tr><th>总计</th><td colspan="2">${price.toFixed(2)}(${count}件商品)</td></tr>`
        }
        // 绑定事件,事件代理
        function bind() {
            var tbody = document.getElementsByTagName('tbody')[0]
            var tfoot = document.getElementsByTagName('tfoot')[0]
            tbody.addEventListener('click', function (e) {
                let num = tbody.children.length
                // 过滤点击的是否是a标签
                if (e.target.tagName === "A") {
                    // 获取数据
                    let price = parseFloat(e.target.parentElement.parentElement.innerHTML.match(/\d+.\d+/)[0])
                    let total = tfoot.innerHTML.match(/\d+.\d+/)[0]
                    e.target.parentElement.parentElement.remove()
                    tfoot.innerHTML = `<tr><th>总计</th><td colspan="2">${(total - price).toFixed(2)}(${num - 1}件商品)</td></tr>`
                }
            })
        }
        // 执行绑定事件
        bind()
    </script>
</body>

</html>

5-11

JS 数组排序

请补全JavaScript代码,根据预设代码中的数组,实现以下功能:

  1. 列表只展示数组中的name属性
  2. 实现点击"销量升序"按钮,列表内容按照销量升序重新渲染
  3. 实现点击"销量降序"按钮,列表内容按照销量降序重新渲染

注意:

  1. 必须使用DOM0级标准事件(onclick)
html
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
    </head>
    <body>
        <button class='up'>销量升序</button>
        <button class='down'>销量降序</button>
        <ul></ul>

        <script>
            var cups = [
                { type: 1, price: 100, color: 'black', sales: 3000, name: '牛客logo马克杯' },
                { type: 2, price: 40, color: 'blue', sales: 1000, name: '无盖星空杯' },
                { type: 4, price: 60, color: 'green', sales: 200, name: '老式茶杯' },
                { type: 3, price: 50, color: 'green', sales: 600, name: '欧式印花杯' }
            ]
            var ul = document.querySelector('ul');
            var upbtn = document.querySelector('.up');
            var downbtn = document.querySelector('.down');
            // 补全代码
             function ulRender() {
                ul.innerHTML = cups.map(v => `<li>${v.name}</li>`).join('');
            }
            upbtn.onclick = function () {
                cups.sort((a, b) => a.sales - b.sales);
                ulRender();
            }
            downbtn.onclick = function () {
                cups.sort((a, b) => b.sales - a.sales);
                ulRender();
            }
        </script>
    </body>
</html>

5-10

JS模块编程题

题目:

完成函数 createModule,调用之后满足如下要求:

1、返回一个对象

2、对象的 greeting 属性值等于 str1, name 属性值等于 str2

3、对象存在一个 sayIt 方法,该方法返回的字符串为 greeting属性值 + ', ' + name属性值

题解:

javascript
function createModule(str1, str2) {
    let res = {
        greeting: str1,
        name: str2,
    }
    res.sayIt = function() {
        return this.greeting + ', ' + this.name;
    }
    return res;
}

5-9

computed 和 watch 的区别?

computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;

watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;

5-8

webpack的构建流程?

1、运行流程

webpack 的运行流程是一个串行的过程,它的工作流程就是将各个插件串联起来

在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条webpack机制中,去改变webpack的运作,使得整个系统扩展性良好

从启动到结束会依次执行以下三大步骤:

  • 初始化流程:从配置文件和 Shell 语句中读取与合并参数,并初始化需要使用的插件和配置插件等执行环境所需要的参数
  • 编译构建流程:从 Entry 发出,针对每个 Module 串行调用对应的 Loader 去翻译文件内容,再找到该 Module 依赖的 Module,递归地进行编译处理
  • 输出流程:对编译后的 Module 组合成 Chunk,把 Chunk 转换成文件,输出到文件系统

img

2、初始化流程

从配置文件和 Shell 语句中读取与合并参数,得出最终的参数

配置文件默认下为webpack.config.js,也或者通过命令的形式指定配置文件,主要作用是用于激活webpack的加载项和插件

webpackwebpack.config.js 中的各个配置项拷贝到 options 对象中,并加载用户配置的 plugins

完成上述步骤之后,则开始初始化Compiler编译对象,该对象掌控者webpack声明周期,不执行具体的任务,只是进行一些调度工作

3、编译构建流程

根据配置中的 entry 找出所有的入口文件

js
module.exports = {
  entry: './src/file.js'
}

初始化完成后会调用Compilerrun来真正启动webpack编译构建流程,主要流程如下:

  • compile 开始编译
  • make 从入口点分析模块及其依赖的模块,创建这些模块对象
  • build-module 构建模块
  • seal 封装构建结果
  • emit 把各个chunk输出到结果文件
compile 编译

执行了run方法后,首先会触发compile,主要是构建一个Compilation对象

该对象是编译阶段的主要执行者,主要会依次下述流程:执行模块创建、依赖收集、分块、打包等主要任务的对象

make 编译模块

当完成了上述的compilation对象后,就开始从Entry入口文件开始读取,主要执行_addModuleChain()函数,如下:

js
_addModuleChain(context, dependency, onModule, callback) {
   // 根据依赖查找对应的工厂函数
   const Dep = /** @type {DepConstructor} */ (dependency.constructor);
   const moduleFactory = this.dependencyFactories.get(Dep);
   
   // 调用工厂函数NormalModuleFactory的create来生成一个空的NormalModule对象
   moduleFactory.create({
       dependencies: [dependency]
   }, (err, module) => {
       const afterBuild = () => {
        this.processModuleDependencies(module, err => {
         if (err) return callback(err);
         callback(null, module);
           });
    };
       
       this.buildModule(module, false, null, null, err => {
           afterBuild();
       })
   })
}

过程如下:

_addModuleChain中接收参数dependency传入的入口依赖,使用对应的工厂函数NormalModuleFactory.create方法生成一个空的module对象

回调中会把此module存入compilation.modules对象和dependencies.module对象中,由于是入口文件,也会存入compilation.entries

随后执行buildModule进入真正的构建模块module内容的过程

build module 完成模块编译

这里主要调用配置的loaders,将我们的模块转成标准的JS模块

在用Loader 对一个模块转换完后,使用 acorn 解析转换后的内容,输出对应的抽象语法树(AST),以方便 Webpack后面对代码的分析

从配置的入口模块开始,分析其 AST,当遇到require等导入其它模块语句时,便将其加入到依赖的模块列表,同时对新找出的依赖模块递归分析,最终搞清所有模块的依赖关系

4、输出流程

seal 输出资源

seal方法主要是要生成chunks,对chunks进行一系列的优化操作,并生成要输出的代码

webpack 中的 chunk ,可以理解为配置在 entry 中的模块,或者是动态引入的模块

根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表

emit 输出完成

在确定好输出内容后,根据配置确定输出的路径和文件名

Compiler 开始生成文件前,钩子 emit 会被执行,这是我们修改最终文件的最后一个机会

从而webpack整个打包过程则结束了

5-7

bind、call、apply 区别?

callapplybind作用是改变函数执行时的上下文,简而言之就是改变函数运行时的this指向

apply

apply接受两个参数,第一个参数是this的指向,第二个参数是函数接受的参数,以数组的形式传入

改变this指向后原函数会立即执行,且此方法只是临时改变this指向一次

call

call方法的第一个参数也是this的指向,后面传入的是一个参数列表

apply一样,改变this指向后原函数会立即执行,且此方法只是临时改变this指向一次

bind

bind方法和call很相似,第一参数也是this的指向,后面传入的也是一个参数列表(但是这个参数列表可以分多次传入)

改变this指向后不会立即执行,而是返回一个永久改变this指向的函数

从上面可以看到,applycallbind三者的区别在于:

  • 三者都可以改变函数的this对象指向
  • 三者第一个参数都是this要指向的对象,如果如果没有这个参数或参数为undefinednull,则默认指向全局window
  • 三者都可以传参,但是apply是数组,而call是参数列表,且applycall是一次性传入参数,而bind可以分为多次传入
  • bind是返回绑定this之后的函数,applycall 则是立即执行

5-6

如何理解this对象?

函数的 this 关键字在 JavaScript 中的表现略有不同,此外,在严格模式和非严格模式之间也会有一些差别

在绝大多数情况下,函数的调用方式决定了 this 的值(运行时绑定)

this 关键字是函数运行时自动生成的一个内部对象,只能在函数内部使用,总指向调用它的对象;同时,this在函数执行过程中,this一旦被确定了,就不可以再更改

javascript
function baz() {
    // 当前调用栈是:baz
    // 因此,当前调用位置是全局作用域
    
    console.log( "baz" );
    bar(); // <-- bar的调用位置
}

function bar() {
    // 当前调用栈是:baz --> bar
    // 因此,当前调用位置在baz中
    
    console.log( "bar" );
    foo(); // <-- foo的调用位置
}

function foo() {
    // 当前调用栈是:baz --> bar --> foo
    // 因此,当前调用位置在bar中
    
    console.log( "foo" );
}

baz(); // <-- baz的调用位置

绑定规则:

根据不同的使用场合,this有不同的值,主要分为下面几种情况:

  • 默认绑定

    严格模式下,不能将全局对象用于默认绑定,this会绑定到undefined,只有函数运行在非严格模式下,默认绑定才能绑定到全局对象

  • 隐式绑定

    函数还可以作为某个对象的方法调用,这时this就指这个上级对象

    特殊情况:

    javascript
    var o = {
        a:10,
        b:{
            a:12,
            fn:function(){
                console.log(this.a); //undefined
                console.log(this); //window
            }
        }
    }
    var j = o.b.fn;
    j();

    此时this指向的是window,这里的大家需要记住,this永远指向的是最后调用它的对象,虽然fn是对象b的方法,但是fn赋值给j时候并没有执行,所以最终指向window

  • new绑定

    通过构建函数new关键字生成一个实例对象,此时this指向这个实例对象

  • 显示绑定

    apply()、call()、bind()是函数的一个方法,作用是改变函数的调用对象。它的第一个参数就表示改变后的调用这个函数的对象。因此,这时this指的就是这第一个参数

5-5

如何理解闭包?

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)

也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域

JavaScript中,每当创建一个函数,闭包就会在函数创建的同时被创建出来,作为函数内部与外部连接起来的一座桥梁

javascript
function init() {
    var name = "Mozilla"; // name 是一个被 init 创建的局部变量
    function displayName() { // displayName() 是内部函数,一个闭包
        alert(name); // 使用了父函数中声明的变量
    }
    displayName();
}
init();

displayName() 没有自己的局部变量。然而,由于闭包的特性,它可以访问到外部函数的变量

5-4

深拷贝浅拷贝的区别?

1、浅拷贝

浅拷贝,指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷贝

如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址

即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址

JavaScript中,存在浅拷贝的现象有:

  • Object.assign
  • Array.prototype.slice(), Array.prototype.concat()
  • 使用拓展运算符实现的复制

2、深拷贝

深拷贝开辟一个新的栈,两个对象属完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性

常见的深拷贝方式有:

  • _.cloneDeep()
  • jQuery.extend()
  • JSON.stringify()
  • 手写循环递归

3、区别

img

浅拷贝和深拷贝都创建出一个新的对象,但在复制对象属性的时候,行为就不一样

浅拷贝只复制属性指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存,修改对象属性会影响原对象;但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象

  • 浅拷贝是拷贝一层,属性为对象时,浅拷贝是复制,两个对象指向同一个地址
  • 深拷贝是递归拷贝深层次,属性为对象时,深拷贝是新开栈,两个对象指向不同的地址

5-3

Vue3中Treeshaking特性?

Tree shaking 是一种通过清除多余代码方式来优化项目打包体积的技术,专业术语叫 Dead code elimination

简单来讲,就是在保持代码运行结果不变的前提下,去除无用的代码

如果把代码打包比作制作蛋糕,传统的方式是把鸡蛋(带壳)全部丢进去搅拌,然后放入烤箱,最后把(没有用的)蛋壳全部挑选并剔除出去

treeshaking则是一开始就把有用的蛋白蛋黄(import)放入搅拌,最后直接作出蛋糕。也就是说 ,tree shaking 其实是找出使用的代码

Tree shaking是基于ES6模板语法(importexports),主要是借助ES6模块的静态编译思想,在编译时就能确定模块的依赖关系,以及输入和输出的变量

Tree shaking无非就是做了两件事:

  • 编译阶段利用ES6 Module判断哪些模块已经加载
  • 判断那些模块和变量未被使用或者引用,进而删除对应代码

通过Tree shakingVue3给我们带来的好处是:

  • 减少程序体积(更小)
  • 减少程序执行时间(更快)
  • 便于将来对程序架构进行优化(更友好)

5-2

SSR解决了什么问题?

SSR主要解决了以下两种问题:

  • seo:搜索引擎优先爬取页面HTML结构,使用ssr时,服务端已经生成了和业务想关联的HTML,有利于seo
  • 首屏呈现渲染:用户无需等待页面所有js加载完成就可以看到页面视图(压力来到了服务器,所以需要权衡哪些用服务端渲染,哪些交给客户端)

但是使用SSR同样存在以下的缺点:

  • 复杂度:整个项目的复杂度
  • 库的支持性,代码兼容
  • 性能问题
    • 每个请求都是n个实例的创建,不然会污染,消耗会变得很大
    • 缓存 node servenginx判断当前用户有没有过期,如果没过期的话就缓存,用刚刚的结果。
    • 降级:监控cpu、内存占用过多,就spa,返回单个的壳
  • 服务器负载变大,相对于前后端分离服务器只需要提供静态资源来说,服务器负载更大,所以要慎重使用

所以在我们选择是否使用SSR前,我们需要慎重问问自己这些问题:

  1. 需要SEO的页面是否只是少数几个,这些是否可以使用预渲染(Prerender SPA Plugin)实现
  2. 首屏的请求响应逻辑是否复杂,数据返回是否大量且缓慢

5-1

SSR是什么?

Server-Side Rendering 我们称其为SSR,意为服务端渲染

由服务侧完成页面的 HTML 结构拼接的页面处理技术,发送到浏览器,然后为其绑定状态与事件,成为完全可交互页面的过程

web的3个阶段的发展史:

  • 传统服务端渲染SSR 网页内容在服务端渲染完成,⼀次性传输到浏览器 打开页面查看源码,浏览器拿到的是全部的dom结构
  • 单页面应用SPA 单页应用优秀的用户体验,使其逐渐成为主流,页面内容由JS渲染出来,这种方式称为客户端渲染 打开页面查看源码,浏览器拿到的仅有宿主元素#app,并没有内容
  • 服务端渲染SSR SSR解决方案,后端渲染出完整的首屏的dom结构返回,前端拿到的内容包括首屏及完整spa结构,应用激活后依然按照spa方式运行

Vue SSR是一个在SPA上进行改良的服务端渲染 通过Vue SSR渲染的页面,需要在客户端激活才能实现交互 Vue SSR将包含两部分:服务端渲染的首屏,包含交互的SPA