Hi~ 很久不见。javascript
上一次整理的100道前端面试题在掘金火了以后,不少小伙伴反馈看答案不方便,其实主要是由于我整理的答案和解析内容很是全面,致使每一道题的篇幅都很长,阅读体验不太好,因此才给你们把答案放到github上。css
最近解锁了掘金的新功能——折叠内容,我将在这篇文章中尽量多的的放置答案和解析。html
公司:京东前端
分类:JavaScriptvue
实际的样式值 能够理解为 浏览器的计算样式java
style 对象中包含支持 style 属性的元素为这个属性设置的样式信息,但不包含从其余样式表层叠继承的一样影响该元素的样式信息。node
DOM2 Style 在 document.defaultView 上增长了 getComputedStyle() 方法。这个方法接收两个参数:要取得计算样式的元素和伪元素字符串(如":after")。若是不须要查询伪元素,则第二个参数能够传 null。getComputedStyle()方法返回一个 CSSStyleDeclaration 对象(与 style 属性的类型同样),包含元素的计算样式。react
<!DOCTYPE html>
<html>
<head>
<title>Computed Styles Example</title>
<style type="text/css"> #myDiv { background-color: blue; width: 100px; height: 200px; } </style>
</head>
<body>
<div id="myDiv" style="background-color: red; border: 1px solid black" ></div>
</body>
<script>
let myDiv = document.getElementById("myDiv");
let computedStyle = document.defaultView.getComputedStyle(myDiv, null);
console.log(computedStyle.backgroundColor); // "red"
console.log(computedStyle.width); // "100px"
console.log(computedStyle.height); // "200px"
console.log(computedStyle.border); // "1px solid black"(在某些浏览器中)
/* 兼容写法 */
function getStyleByAttr(obj, name) {
return window.getComputedStyle
? window.getComputedStyle(obj, null)[name]
: obj.currentStyle[name];
}
let node = document.getElementById("myDiv");
console.log(getStyleByAttr(node, "backgroundColor"));
console.log(getStyleByAttr(node, "width"));
console.log(getStyleByAttr(node, "height"));
console.log(getStyleByAttr(node, "border
</script>
</html>
复制代码
公司:高德、头条nginx
分类:Reactgit
Hook 是 React 16.8 的新增特性。它可让你在不编写 class 的状况下使用 state 以及其余的 React 特性。
从官网的这句话中,咱们能够明确的知道,Hook
增长了函数式组件中state
的使用,在以前函数式组件是没法拥有本身的状态,只能经过props
以及context
来渲染本身的UI
,而在业务逻辑中,有些场景必需要使用到state
,那么咱们就只能将函数式组件定义为class
组件。而如今经过Hook
,咱们能够轻松的在函数式组件中维护咱们的状态,不须要更改成class
组件。
React16.8 加入 hooks,让 React 函数式组件更加灵活
hooks 以前,React 存在不少问题
hooks 很好的解决了上述问题,hooks 提供了不少方法
React Hooks
要解决的问题是状态共享,这里的状态共享是指只共享状态逻辑复用,并非指数据之间的共享。咱们知道在React Hooks
以前,解决状态逻辑复用问题,咱们一般使用higher-order components
和render-props
。
既然已经有了这两种解决方案,为何React
开发者还要引入React Hook
?对于higher-order components
和render-props
,React Hook
的优点在哪?
PS:Hook 最大的优点其实仍是对于状态逻辑的复用便捷,还有代码的简洁,以及帮助函数组件加强功能,
咱们先来看一下React
官方给出的React Hook
的demo
import { useState } from "React";
function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
return (
<div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}>Click me</button> </div>
);
}
复制代码
咱们再来看看不用React Hook
的话,如何实现
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
render() {
return (
<div> <p>You clicked {this.state.count} times</p> <button onClick={() => this.setState({ count: this.state.count + 1 })}> Click me </button> </div>
);
}
}
复制代码
能够看到,在React Hook
中,class Example
组件变成了函数式组件,可是这个函数式组件却拥有的本身的状态,同时还能够更新自身的状态。这一切都得益于useState
这个Hook
,useState
会返回一对值:当前状态和一个让你更新它的函数,你能够在事件处理函数中或其余一些地方调用这个函数。它相似 class
组件的 this.setState
,可是它不会把新的 state
和旧的 state
进行合并
Hooks 的基本类型:
type Hooks = {
memoizedState: any, // 指向当前渲染节点 Fiber
baseState: any, // 初始化 initialState, 已经每次 dispatch 以后 newState
baseUpdate: Update<any> | null, // 当前须要更新的 Update ,每次更新完以后,会赋值上一个 update,方便 react 在渲染错误的边缘,数据回溯
queue: UpdateQueue<any> | null, // UpdateQueue 经过
next: Hook | null, // link 到下一个 hooks,经过 next 串联每一 hooks
};
type Effect = {
tag: HookEffectTag, // effectTag 标记当前 hook 做用在 life-cycles 的哪个阶段
create: () => mixed, // 初始化 callback
destroy: (() => mixed) | null, // 卸载 callback
deps: Array<mixed> | null,
next: Effect, // 同上
};
复制代码
React Hooks 全局维护了一个 workInProgressHook 变量,每一次调取 Hooks API 都会首先调取 createWorkInProgressHooks 函数。
function createWorkInProgressHook() {
if (workInProgressHook === null) {
// This is the first hook in the list
if (firstWorkInProgressHook === null) {
currentHook = firstCurrentHook;
if (currentHook === null) {
// This is a newly mounted hook
workInProgressHook = createHook();
} else {
// Clone the current hook.
workInProgressHook = cloneHook(currentHook);
}
firstWorkInProgressHook = workInProgressHook;
} else {
// There's already a work-in-progress. Reuse it.
currentHook = firstCurrentHook;
workInProgressHook = firstWorkInProgressHook;
}
} else {
if (workInProgressHook.next === null) {
let hook;
if (currentHook === null) {
// This is a newly mounted hook
hook = createHook();
} else {
currentHook = currentHook.next;
if (currentHook === null) {
// This is a newly mounted hook
hook = createHook();
} else {
// Clone the current hook.
hook = cloneHook(currentHook);
}
}
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
} else {
// There's already a work-in-progress. Reuse it.
workInProgressHook = workInProgressHook.next;
currentHook = currentHook !== null ? currentHook.next : null;
}
}
return workInProgressHook;
}
复制代码
假设咱们须要执行如下 hooks 代码:
function FunctionComponet() {
const [ state0, setState0 ] = useState(0);
const [ state1, setState1 ] = useState(1);
useEffect(() => {
document.addEventListener('mousemove', handlerMouseMove, false);
...
...
...
return () => {
...
...
...
document.removeEventListener('mousemove', handlerMouseMove, false);
}
})
const [ satte3, setState3 ] = useState(3);
return [state0, state1, state3];
}
复制代码
当咱们了解 React Hooks 的简单原理,获得 Hooks 的串联不是一个数组,可是是一个链式的数据结构,从根节点 workInProgressHook 向下经过 next 进行串联。这也就是为何 Hooks 不能嵌套使用,不能在条件判断中使用,不能在循环中使用。不然会破坏链式结构。
函数组件 的本质是函数,没有 state 的概念的,所以不存在生命周期一说,仅仅是一个 render 函数而已。
可是引入 Hooks 以后就变得不一样了,它能让组件在不使用 class 的状况下拥有 state,因此就有了生命周期的概念,所谓的生命周期其实就是 useState、 useEffect() 和 useLayoutEffect() 。
即:Hooks 组件(使用了 Hooks 的函数组件)有生命周期,而函数组件(未使用 Hooks 的函数组件)是没有生命周期的。
下面,是具体的 class 与 Hooks 的生命周期对应关系:
公司:头条
分类:Css
meida queries 的方式能够说是我早期采用的布局方式,它主要是经过查询设备的宽度来执行不一样的 css 代码,最终达到界面的配置。
核心语法:
@media only screen and (max-width: 374px) {
/* iphone5 或者更小的尺寸,以 iphone5 的宽度(320px)比例设置样式*/
}
@media only screen and (min-width: 375px) and (max-width: 413px) {
/* iphone6/7/8 和 iphone x */
}
@media only screen and (min-width: 414px) {
/* iphone6p 或者更大的尺寸,以 iphone6p 的宽度(414px)比例设置样式 */
}
复制代码
优势:
缺点:
以天猫的实现方式进行说明:
它的 viewport 是固定的:<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
高度定死,宽度自适应,元素都采用 px 作单位。
随着屏幕宽度变化,页面也会跟着变化,效果就和 PC 页面的流体布局差很少,在哪一个宽度须要调整的时候使用响应式布局调调就行(好比网易新闻),这样就实现了『适配』。
实现原理:
根据 rem 将页面放大 dpr 倍, 而后 viewport 设置为 1/dpr.
这样整个网页在设备内显示时的页面宽度就会等于设备逻辑像素大小,也就是 device-width。这个 device-width 的计算公式为:
设备的物理分辨率/(devicePixelRatio * scale)
,在 scale 为 1 的状况下,device-width = 设备的物理分辨率/devicePixelRatio
。
rem
是相对长度单位,rem
方案中的样式设计为相对于根元素font-size
计算值的倍数。根据屏幕宽度设置html
标签的font-size
,在布局时使用 rem 单位布局,达到自适应的目的。
viewport 是固定的:<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
。
经过如下代码来控制 rem 基准值(设计稿以 720px 宽度量取实际尺寸)
!(function (d) {
var c = d.document;
var a = c.documentElement;
var b = d.devicePixelRatio;
var f;
function e() {
var h = a.getBoundingClientRect().width,
g;
if (b === 1) {
h = 720;
}
if (h > 720) h = 720; //设置基准值的极限值
g = h / 7.2;
a.style.fontSize = g + "px";
}
if (b > 2) {
b = 3;
} else {
if (b > 1) {
b = 2;
} else {
b = 1;
}
}
a.setAttribute("data-dpr", b);
d.addEventListener(
"resize",
function () {
clearTimeout(f);
f = setTimeout(e, 200);
},
false
);
e();
})(window);
复制代码
css 经过 sass 预编译,设置量取的 px 值转化 rem 的变量$px: (1/100)+rem;
优势:
缺点:
js
脚本监听分辨率的变化来动态改变根元素的字体大小,css
样式和 js
代码有必定耦合性,而且必须将改变font-size
的代码放在 css
样式以前。rem
计算后可能会出现小数像素,浏览器会对这部分小数四舍五入,按照整数渲染,有可能没那么准确。视口是浏览器中用于呈现网页的区域。
虽然 vw 能更优雅的适配,可是仍是有点小问题,就是宽,高无法限制。
$base_vw = 375;
@function vw ($px) {
return ($px/$base_vw) * 100vw
};
复制代码
优势:
css
移动端适配方案,不存在脚本依赖问题。rem
以根元素字体大小的倍数定义元素大小,逻辑清晰简单。缺点:
// scss 语法
// 设置html根元素的大小 750px->75 640px->64
// 将屏幕分红10份,每份做为根元素的大小。
$vw_fontsize: 75
@function rem($px) {
// 例如:一个div的宽度为100px,那么它对应的rem单位就是(100/根元素的大小)* 1rem
@return ($px / $vw_fontsize) * 1rem;
}
$base_design: 750
html {
// rem与vw相关联
font-size: ($vw_fontsize / ($base_design / 2)) * 100vw;
// 同时,经过Media Queries 限制根元素最大最小值
@media screen and (max-width: 320px) {
font-size: 64px;
}
@media screen and (min-width: 540px) {
font-size: 108px;
}
}
// body 也增长最大最小宽度限制,避免默认100%宽度的 block 元素跟随 body 而过大太小
body {
max-width: 540px;
min-width: 320px;
}
复制代码
使用百分比%定义宽度,高度用px
固定,根据可视区域实时尺寸进行调整,尽量适应各类分辨率,一般使用max-width
/min-width
控制尺寸范围过大或者太小。
优势:
缺点:
var arr =[[‘A’,’B’],[‘a’,’b’],[1,2]]
求二维数组的全排列组合 结果:Aa1,Aa2,Ab1,Ab2,Ba1,Ba2,Bb1,Bb2公司:美团
分类:算法
function foo(arr) {
// 用于记录初始数组长度, 用于将数组前两组已经获取到全排列的数组进行截取标识
var len = arr.length;
// 当递归操做后, 数组长度为1时, 直接返回arr[0], 只有大于1继续处理
if (len >= 2) {
// 每次只作传入数组的前面两个数组进行全排列组合, 即arr[0]和arr[1]的全排列组合
var len1 = arr[0].length;
var len2 = arr[1].length;
var items = new Array(len1 * len2); // 建立全排列组合有可能次数的数组
var index = 0; // 记录每次全排列组合后的数组下标
for (var i = 0; i < len1; i++) {
for (var j = 0; j < len2; j++) {
if (Array.isArray(arr[0])) {
// 当第二次进来后, 数组第一个元素一定是数组包着数组
items[index] = arr[0][i].concat(arr[1][j]); // 对于已是第二次递归进来的全排列直接追加便可
} else {
items[index] = [arr[0][i]].concat(arr[1][j]); // 这里由于只须要去arr[0]和arr[1]的全排列, 因此这里就直接使用concat便可
}
index++; // 更新全排列组合的下标
}
}
// 若是数组大于2, 这里新的newArr作一个递归操做
var newArr = new Array(len - 1); // 递归的数组比传进来的数组长度少一, 由于上面已经将传进来的数组的arr[0]和arr[1]进行全排列组合, 因此这里的newArr[0]就是上面已经全排列好的数组item
for (var i = 2; i < arr.length; i++) {
// 这里的for循环是为了截取下标1项后的数组进行赋值给newArr
newArr[i - 1] = arr[i];
}
newArr[0] = items; // 由于上面已经将传进来的数组的arr[0]和arr[1]进行全排列组合, 因此这里的newArr[0]就是上面已经全排列好的数组item
// 从新组合后的数组进行递归操做
return foo(newArr);
} else {
// 当递归操做后, 数组长度为1时, 直接返回arr[0],
return arr[0];
}
}
var arr = [
["A", "B"],
["a", "b"],
[1, 2],
];
console.log(foo(arr));
复制代码
const getResult = (arr1, arr2) => {
if (!Array.isArray(arr1) || !Array.isArray(arr2)) {
return;
}
if (!arr1.length) {
return arr2;
}
if (!arr2.length) {
return arr1;
}
let result = [];
for (let i = 0; i < arr1.length; i++) {
for (let j = 0; j < arr2.length; j++) {
result.push(String(arr1[i]) + String(arr2[j]));
}
}
return result;
};
const findAll = (arr) =>
arr.reduce((total, current) => {
return getResult(total, current);
}, []);
var arr = [
["A", "B"],
["a", "b"],
[1, 2],
];
console.log(findAll(arr));
复制代码
var arr = [
["A", "B"],
["a", "b"],
[1, 2],
];
let res = [],
lengthArr = arr.map((d) => d.length);
let indexArr = new Array(arr.length).fill(0);
function addIndexArr() {
indexArr[0] = indexArr[0] + 1;
let i = 0;
let overflow = false;
while (i <= indexArr.length - 1) {
if (indexArr[i] >= lengthArr[i]) {
if (i < indexArr.length - 1) {
indexArr[i] = 0;
indexArr[i + 1] = indexArr[i + 1] + 1;
} else {
overflow = true;
}
}
i++;
}
return overflow;
}
function getAll(arr, indexArr) {
let str = "";
arr.forEach((item, index) => {
str += item[indexArr[index]];
});
res.push(str);
let overflow = addIndexArr();
if (overflow) {
return;
} else {
return getAll(arr, indexArr);
}
}
getAll(arr, indexArr);
console.log(res);
复制代码
公司:腾讯微视
分类:工程化
[改动文件类型]:[改动说明]
.postcssrc.js
.babelrc
.prettierrc
(vscode 插件 prettier-code fomatter)— 注意与 eslint 要保持一致.editorconfig
.eslintrc.js
(强制开启验证模式)上线准备 a. 域名申请 b. 备案申请 c. 服务器申请 d. 部署
测试线上环境
日志监控
公司:水滴筹
分类:Vue
// Parent.vue
<Child @mounted="doSomething"/>
// Child.vue
mounted() {
this.$emit("mounted");
}
复制代码
// Parent.vue
<Child @hook:mounted="doSomething" ></Child>
doSomething() {
console.log('父组件监听到 mounted 钩子函数 ...');
},
// Child.vue
mounted(){
console.log('子组件触发 mounted 钩子函数 ...');
},
// 以上输出顺序为:
// 子组件触发 mounted 钩子函数 ...
// 父组件监听到 mounted 钩子函数 ...
复制代码
分类:Vue
缺点:调用这个方法的时候没有提示
// global.js
const RandomString = (encode = 36, number = -8) => {
return Math.random() // 生成随机数字, eg: 0.123456
.toString(encode) // 转化成36进制 : "0.4fzyo82mvyr"
.slice(number);
},
export default {
RandomString,
...
}
复制代码
// 在项目入口的main.js里配置
import Vue from "vue";
import global from "@/global";
Object.keys(global).forEach((key) => {
Vue.prototype["$global" + key] = global[key];
});
复制代码
// 挂载以后,在须要引用全局变量的模块处(App.vue),不需再导入全局变量模块,而是直接用this就能够引用了,以下:
export default {
mounted() {
this.$globalRandomString();
},
};
复制代码
优势:由于mixin里面的methods会和建立的每一个单文件组件合并。这样作的优势是调用这个方法的时候有提示
// mixin.js
import moment from 'moment'
const mixin = {
methods: {
minRandomString(encode = 36, number = -8) {
return Math.random() // 生成随机数字, eg: 0.123456
.toString(encode) // 转化成36进制 : "0.4fzyo82mvyr"
.slice(number);
},
...
}
}
export default mixin
复制代码
// 在项目入口的main.js里配置
import Vue from 'vue'
import mixin from '@/mixin'
Vue.mixin(mixin)
复制代码
export default {
mounted() {
this.minRandomString()
}
}
复制代码
Vue.use的实现没有挂载的功能,只是触发了插件的install方法,本质仍是使用了Vue.prototype。
// plugin.js
function randomString(encode = 36, number = -8) {
return Math.random() // 生成随机数字, eg: 0.123456
.toString(encode) // 转化成36进制 : "0.4fzyo82mvyr"
.slice(number);
}
const plugin = {
// install 是默认的方法。
// 当外界在 use 这个组件或函数的时候,就会调用自己的 install 方法,同时传一个 Vue 这个类的参数。
install: function(Vue){
Vue.prototype.$pluginRandomString = randomString
...
},
}
export default plugin
复制代码
// 在项目入口的main.js里配置
import Vue from 'vue'
import plugin from '@/plugin'
Vue.use(plugin)
复制代码
export default {
mounted() {
this.$pluginRandomString()
}
}
复制代码
// 建立全局方法
this.$root.$on("test", function () {
console.log("test");
});
// 销毁全局方法
this.$root.$off("test");
// 调用全局方法
this.$root.$emit("test");
复制代码
公司:极光推送
分类:Vue
vm.$set()
解决了什么问题在 Vue.js 里面只有 data 中已经存在的属性才会被 Observe 为响应式数据,若是你是新增的属性是不会成为响应式数据,所以 Vue 提供了一个 api(vm.$set)来解决这个问题。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Vue Demo</title>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
</head>
<body>
<div id="app">
{{user.name}} {{user.age}}
<button @click="addUserAgeField">增长一个年纪字段</button>
</div>
<script> const app = new Vue({ el: "#app", data: { user: { name: "test", }, }, mounted() {}, methods: { addUserAgeField() { // this.user.age = 20 这样是不起做用, 不会被Observer this.$set(this.user, "age", 20); // 应该使用 }, }, }); </script>
</body>
</html>
复制代码
vm.$set()在 new Vue()时候就被注入到 Vue 的原型上。
vue/src/core/instance/index.js
import { initMixin } from "./init";
import { stateMixin } from "./state";
import { renderMixin } from "./render";
import { eventsMixin } from "./events";
import { lifecycleMixin } from "./lifecycle";
import { warn } from "../util/index";
function Vue(options) {
if (process.env.NODE_ENV !== "production" && !(this instanceof Vue)) {
warn("Vue is a constructor and should be called with the `new` keyword");
}
this._init(options);
}
initMixin(Vue);
// 给原型绑定代理属性$props, $data
// 给Vue原型绑定三个实例方法: vm.$watch,vm.$set,vm.$delete
stateMixin(Vue);
// 给Vue原型绑定事件相关的实例方法: vm.$on, vm.$once ,vm.$off , vm.$emit
eventsMixin(Vue);
// 给Vue原型绑定生命周期相关的实例方法: vm.$forceUpdate, vm.destroy, 以及私有方法_update
lifecycleMixin(Vue);
// 给Vue原型绑定生命周期相关的实例方法: vm.$nextTick, 以及私有方法_render, 以及一堆工具方法
renderMixin(Vue);
export default Vue;
复制代码
Vue.prototype.$set = set;
Vue.prototype.$delete = del;
复制代码
源码位置: vue/src/core/observer/index.js
export function set(target: Array<any> | Object, key: any, val: any): any {
// 1.类型判断
// 若是 set 函数的第一个参数是 undefined 或 null 或者是原始类型值,那么在非生产环境下会打印警告信息
// 这个api原本就是给对象与数组使用的
if (
process.env.NODE_ENV !== "production" &&
(isUndef(target) || isPrimitive(target))
) {
warn(
`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`
);
}
// 2.数组处理
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 相似$vm.set(vm.$data.arr, 0, 3)
// 修改数组的长度, 避免索引>数组长度致使splcie()执行有误
//若是不设置length,splice时,超过本来数量的index则不会添加空白项
target.length = Math.max(target.length, key);
// 利用数组的splice变异方法触发响应式, 这个前面讲过
target.splice(key, 1, val);
return val;
}
//3.对象,且key不是原型上的属性处理
// target为对象, key在target或者target.prototype上。
// 同时必须不能在 Object.prototype 上
// 直接修改便可, 有兴趣能够看issue: https://github.com/vuejs/vue/issues/6845
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val;
}
// 以上都不成立, 即开始给target建立一个全新的属性
// 获取Observer实例
const ob = (target: any).__ob__;
// Vue 实例对象拥有 _isVue 属性, 即不容许给Vue 实例对象添加属性
// 也不容许Vue.set/$set 函数为根数据对象(vm.$data)添加属性
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== "production" &&
warn(
"Avoid adding reactive properties to a Vue instance or its root $data " +
"at runtime - declare it upfront in the data option."
);
return val;
}
//5.target是非响应式数据时
// target自己就不是响应式数据, 直接赋值
if (!ob) {
target[key] = val;
return val;
}
//6.target对象是响应式数据时
//定义响应式对象
defineReactive(ob.value, key, val);
//watcher执行
ob.dep.notify();
return val;
}
复制代码
// 判断给定变量是不是未定义,当变量值为 null时,也会认为其是未定义
export function isUndef(v: any): boolean %checks {
return v === undefined || v === null;
}
// 判断给定变量是不是原始类型值
export function isPrimitive(value: any): boolean %checks {
return (
typeof value === "string" ||
typeof value === "number" ||
// $flow-disable-line
typeof value === "symbol" ||
typeof value === "boolean"
);
}
// 判断给定变量的值是不是有效的数组索引
export function isValidArrayIndex(val: any): boolean {
const n = parseFloat(String(val));
return n >= 0 && Math.floor(n) === n && isFinite(val);
}
复制代码
export function observe(value: any, asRootData: ?boolean): Observer | void {
// 省略...
if (asRootData && ob) {
// vue已经被Observer了,而且是根数据对象, vmCount才会++
ob.vmCount++;
}
return ob;
}
复制代码
export function initState(vm: Component) {
vm._watchers = [];
const opts = vm.$options;
if (opts.props) initProps(vm, opts.props);
if (opts.methods) initMethods(vm, opts.methods);
if (opts.data) {
//opts.data为对象属性
initData(vm);
} else {
observe((vm._data = {}), true /* asRootData */);
}
if (opts.computed) initComputed(vm, opts.computed);
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
复制代码
function initData(vm: Component) {
let data = vm.$options.data;
data = vm._data = typeof data === "function" ? getData(data, vm) : data || {};
// 省略...
// observe data
observe(data, true /* asRootData */);
}
复制代码
从源码能够看出 set 主要逻辑以下:
vm.$set(target、key、value)
公司:极光推送
分类:JavaScript
看个例子
function deepCopy(obj){
const res = Array.isArray(obj) ? [] : {};
for(let key in obj){
if(typeof obj[key] === 'object'){
res[key] = deepCopy(obj[key]);
}else{
res[key] = obj[key];
}
}
return res
}
var obj = {
a:1,
b:2,
c:[1,2,3],
d:{aa:1,bb:2},
};
obj.e = obj;
console.log('obj',obj); // 不会报错
const objCopy = deepCopy(obj);
console.log(objCopy); //Uncaught RangeError: Maximum call stack size exceeded
复制代码
从例子能够看到,当存在循环引用的时候,deepCopy会报错,栈溢出。
你们都知道,对象的key是不能是对象的。
{{a:1}:2}
// Uncaught SyntaxError: Unexpected token ':'
复制代码
参考解决方式一:使用weekmap:
解决循环引用问题,咱们能够额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系
这个存储空间,须要能够存储key-value
形式的数据,且key
能够是一个引用类型,
咱们能够选择 WeakMap
这种数据结构:
WeakMap
中有无克隆过的对象key
,克隆对象做为value
进行存储function isObject(obj) {
return (typeof obj === 'object' || typeof obj === 'function') && obj !== null
}
function cloneDeep(source, hash = new WeakMap()) {
if (!isObject(source)) return source;
if (hash.has(source)) return hash.get(source); // 新增代码,查哈希表
var target = Array.isArray(source) ? [] : {};
hash.set(source, target); // 新增代码,哈希表设值
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
if (isObject(source[key])) {
target[key] = cloneDeep(source[key], hash); // 新增代码,传入哈希表
} else {
target[key] = source[key];
}
}
}
return target;
}
复制代码
参考解决方式二:
能够用 Set,发现相同的对象直接赋值,也可用 Map
const o = { a: 1, b: 2 };
o.c = o;
function isPrimitive(val) {
return Object(val) !== val;
}
const set = new Set();
function clone(obj) {
const copied = {};
for (const [key, value] of Object.entries(obj)) {
if (isPrimitive(value)) {
copied[key] = value;
} else {
if (set.has(value)) {
copied[key] = { ...value };
} else {
set.add(value);
copied[key] = clone(value);
}
}
}
return copied;
}
复制代码
公司:编程猫
分类:工程化
有单元测试加持能够保障交付代码质量,加强本身和他人的信心。咱们选择第三方库的时候不也是会优先选择有测试保障的吗?将来对代码进行改动时也能够节省回归测试的时间。
在作单元测试时尽可能以集成测试为主,对少许难以被集成测试覆盖或须要分发的代码作单元测试,同时也能够有少许的端到端测试辅助。
尽可能不测试代码实现,测试代码实现可能会让测试用例很快就失效。好比断言变量,当变量名发生变动时会致使测试不经过,可是可能功能并无发生改变。
以用户视角测试程序的功能,而非上帝视角。
对一个组件,传入不一样参数渲染 dom ,对用户而言可能能够看到某些特定文字或能够作某些操做。此时能够断言 dom 是否出现了某些字,作动做(如点击、输入 、提交表单等)是否有正确的响应。
不要测试实现细节。好比以上帝视角检查 redux store
上的数据、state
的数据等,而这些在最终用户眼里是不存在的,用户能感知的只是所表现的功能。
Jest
是 facebook 出品的测试框架。开箱即用,自带断言库、mock、覆盖率报告等功能。
因为前端要测试 UI,须要在模拟浏览器环境中渲染出 dom,因此须要一个这样的库。存在不少这样的库,经常使用的有 Enzyme
、@testing-library/react
。
Jest
自带测试报告,可是众多的项目分散在 gitlab 中给查看报告带来了麻烦。须要考虑有一个集中的地方查看测试报告。这里结合了 sonar
和 reportportal
归集测试报告,能够经过一个集中的地方查看全部项目的测试报告。
其中结合 sonar
的代码扫描功能能够查看测试覆盖率等报告信息。reportportal
能够查看测试执行率,另外官方宣称自带 AI 分析报告,能够得出多维度的统计信息。
公司:编程猫
分类:React
虽然 React 自己有些函数式味道,但为了迎合用户习惯,早期只提供了 React.createClass() API 来定义组件: 天然而然地,(类)继承就成了一种直觉性的尝试。而在 JavaScript 基于原型的扩展模式下,相似于继承的 Mixin 方案就成了首选:
// 定义Mixin
var Mixin1 = {
getMessage: function () {
return "hello world";
},
};
var Mixin2 = {
componentDidMount: function () {
console.log("Mixin2.componentDidMount()");
},
};
// 用Mixin来加强现有组件
var MyComponent = React.createClass({
mixins: [Mixin1, Mixin2],
render: function () {
return <div>{this.getMessage()}</div>;
},
});
复制代码
但存在诸多缺陷
组件与 Mixin 之间存在隐式依赖(Mixin 常常依赖组件的特定方法,但在定义组件时并不知道这种依赖关系)多个 Mixin 之间可能产生冲突(好比定义了相同的 state 字段)Mixin 倾向于增长更多状态,这下降了应用的可预测性(The more state in your application, the harder it is to reason about it.),致使复杂度剧增。
隐式依赖致使依赖关系不透明,维护成本和理解成本迅速攀升:难以快速理解组件行为,须要全盘了解全部依赖 Mixin 的扩展行为,及其之间的相互影响。组件自身的方法和 state 字段不敢轻易删改,由于难以肯定有没有 Mixin 依赖它 Mixin 也难以维护,由于 Mixin 逻辑最后会被打平合并到一块儿,很难搞清楚一个 Mixin 的输入输出。
毫无疑问,这些问题是致命的 因此,React v0.13.0 放弃了 Mixin(继承),转而走向 HOC(组合)。
// 定义高阶组件
var Enhance = (ComposedComponent) =>
class extends Component {
constructor() {
this.state = { data: null };
}
componentDidMount() {
this.setState({ data: "Hello" });
}
render() {
return <ComposedComponent {...this.props} data={this.state.data} />;
}
};
class MyComponent {
render() {
if (!this.data) return <div>Waiting...</div>;
return <div>{this.data}</div>;
}
}
// 用高阶组件来加强普通组件,进而实现逻辑复用
export default Enhance(MyComponent);
复制代码
理论上,只要接受组件类型参数并返回一个组件的函数都是高阶组件((Component, ...args) => Component
),但为了方便组合,推荐Component => Component
形式的 HOC,经过偏函数应用来传入其它参数,例如:React Redux's connect
const ConnectedComment = connect(commentSelector, commentActions)(CommentList);
优势:
缺点:HOC 虽然没有那么多致命问题,但也存在一些小缺陷:
“render prop” 是指⼀种在 React 组件之间使⽤⼀个值为函数的 prop 共享代码的简单技术;
class Mouse extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}
handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY,
});
}
render() {
return (
<div style={{ height: "100%" }} onMouseMove={this.handleMouseMove}> {/* Instead of providing a static representation of what <Mouse> renders, use the `render` prop to dynamically determine what to render. */} {this.props.render(this.state)} </div> ); } } 复制代码
优势:数据共享、代码复⽤,将组件内的 state 做为 props 传递给调⽤者,将渲染逻辑交给调⽤者
缺点:⽆法在 return 语句外访问数据、嵌套写法不够优雅;
function MyResponsiveComponent() {
const width = useWindowWidth();
// Our custom Hook
return <p>Window width is {width}</p>;
}
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
});
return width;
}
复制代码
比起上面提到的其它方案,Hooks 让组件内逻辑复用再也不与组件复用捆绑在一块儿,是真正在从下层去尝试解决(组件间)细粒度逻辑的复用问题。
此外,这种声明式逻辑复用方案将组件间的显式数据流与组合思想进一步延伸到了组件内,契合 React 理念。
优势以下:
Hooks 也并不是完美,只是就目前而言,其缺点以下:
let [nums, setNums] = useState([0,1,2]); nums.push(1) 无效,必须使用 nums=[...nums, 1]
,再 setNums(nums);类组件中直接 push 是没问题的公司:编程猫
分类:JavaScript
任何一个组件都应该遵照一套标准,能够使得不一样区域的开发人员据此标准开发出一套标准统一的组件
Single Point Of Truth,就是尽可能不要重复代码,出自《The Art of Unix Programming》
使用父组件的 state 控制子组件的状态而不是直接经过 ref 操做子组件
父组件不依赖子组件,删除某个子组件不会形成功能异常
除了数据,避免复杂的对象,尽可能只接收原始类型的值
function F() {
this.a = 1;
}
var obj = new F();
console.log(obj.prototype);
复制代码
公司:富途
分类:JavaScript
undefined
复制代码
构造函数实例通常没有prototype属性。除了Function构造函数
只有函数才有prototype属性,这个属性值为一个object对象 实例对象时没有这个属性
实例对象经过__proto__这个内部属性([[prototype]]
)来串起一个原型链的,经过这个原型链能够查找属性,
方法 经过new操做符初始化一个函数对象的时候就会构建出一个实例对象,
函数对象的prototype属性指向的对象就是这个实例对象的原型对象,也就是__proto__指向的对象
经典与原型链图
公司:富途
分类:JavaScript
十万次循环代码插入 body 中,页面会出现卡顿,代码后的 DOM 节点加载不出来
设置 script 标签 defer 属性,浏览器其它线程将下载脚本,待到文档解析完成脚本才会执行。
若 button 中的点击事件在 defer 脚本前定义,则在 defer 脚本加载完后,响应点击事件。
若 button 中的点击事件在 defer 脚本后定义,则用户点击 button 无反应,待脚本加载完后,再次点击有响应。
代码示例
<!-- test.html -->
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<div class="test1">test1</div>
<div id="hello"></div>
<script> // 待defer脚本下载完成后响应 function alertMsg() { alert("123"); } </script>
<input type="button" id="button1" onclick="alertMsg()" />
<script src="./test.js" defer></script>
<div class="test2">test2</div>
</body>
<style> .test1 { color: red; font-size: 50px; } .test2 { color: yellow; font-size: 50px; } </style>
</html>
复制代码
// test.js
for (let i = 0; i < 100000; i++) {
console.log(i);
}
document.getElementById("hello").innerHTML = "hello world";
复制代码
Concurrent.Thread.create(function () {
$("#test").click(function () {
alert(1);
});
for (var i = 0; i < 100000; i++) {
console.log(i);
}
});
复制代码
若该情形是渲染十万条数据的状况下,则能够使用虚拟列表。虚拟列表即只渲染可视区域的数据,使得在数据量庞大的状况下,减小 DOM 的渲染,使得列表流畅地无限滚动。
实现方案:
基于虚拟列表是渲染可视区域的特性,咱们须要作到如下三点
topHeight 的计算比较简单,就是滚动了多少高度,topHeight=scrollTop。
start 的计算依赖于 topHeight 和每项元素的高度 itemHeight,假设咱们向上移动了两个列表项,则 start 为 2,如此,咱们有 start = Math.floor(topHeight / itemHeight)
。
end 的计算依赖于屏幕的高度能显示多少个列表项,咱们称之为 visibleCount,则有 visibleCount = Math.ceil(clientHeight / itemHeight)
,向上取整是为了不计算偏小致使屏幕没有显示足够的内容,则 end = start + visibleCount
。 bottomHeight 须要咱们知道整个列表没有被截断前的高度,减去其顶部的高度,计算顶部的高度有了 end 就很简单了,假设咱们的整个列表项的数量为 totalItem,则 bottomHeight = (totalItem - end - 1) \* itemHeight
。
会出现的问题:
可是当这样实现的时候,会发现有两个问题:
来分析下,对于第一个问题,会出现留白的状况,那么咱们能够在顶部或者底部预留必定的位置,而第二个问题,也是能够经过在顶部和底部预留必定的空间,因此解决这个问题只要一个方案就能够解决了,那就是顶部和底部都预留必定的位置。
假设 reserveTop 为顶部预留的位置数,reserveBottom 为底部预留的位置数,那么咱们上面的数据的计算就要从新定义了,具体如何计算,请看下图。
reserveTop 和 reserveBottom 尽可能大点(固然也不要太大),或者知道列表项的最高高度为多少,就按这个最高高度来。当你发现你滚动的时候顶部有留白,就调大 reserveTop 的数值,当你发现滚动的时候底部有留白,那就调大 reserveBottom 的数值。
公司:富途
分类:其它
7 只
复制代码
每一个老鼠只有死或活 2 种状态,所以每一个老鼠能够看做一个 bit,取 0 或 1N 个老鼠能够看做 N 个 bit,能够表达 2^N 种状态(其中第 n 个状态表明第 n 个瓶子有毒)所以全部老鼠能表示的状态数能大于等于 100 便可。
代码实现
let n = 1;
while (Math.pow(2, n) < 100) {
n++;
}
console.log(n);
复制代码
通俗点的理解:
给 100 个瓶分别标上以下标签(7 位长度): 0000001 (第 1 瓶) 0000010 (第 2 瓶) 0000011 (第 3 瓶) ...... 1100100 (第 100 瓶)
从编号最后 1 位是 1 的全部的瓶子里面取出 1 滴混在一块儿(好比从第一瓶,第三瓶,。。。里分别取出一滴混在一块儿)并标上记号为 1。以此类推,从编号第一位是 1 的全部的瓶子里面取出 1 滴混在一块儿并标上记号为 7。如今获得有 7 个编号的混合液,小白鼠排排站,分别标上 7,6,。。。1 号,并分别给它们灌上对应号码的混合液。三天过去了,过来验尸吧:
从左到右,死了的小白鼠贴上标签 1,没死的贴上 0,最后获得一个序号,把这个序号换成 10 进制的数字,就是有毒的那瓶水的编号。
检验一下:假如第一瓶有毒,按照 0000001 (第 1 瓶),说明第 1 号混合液有毒,所以小白鼠的生死符为 0000001(编号为 1 的小白鼠挂了),0000001 二进制标签转换成十进制=1 号瓶有毒;假如第三瓶有毒,0000011 (第 3 瓶),第 1 号和第 2 号混合液有毒,所以小白鼠的生死符为 0000011(编号为 1,2 的鼠兄弟挂了),0000011 二进制标签转换成十进制=3 号瓶有毒。
因此结果就是 2^7 = 128 >= 100,至少须要 7 只小白鼠。
公司:快手
分类:JavaScript
const resolved = Promise.resolve(42);
const rejected = Promise.reject(-1);
const allSettledPromise = Promise.allSettled([resolved, rejected]);
allSettledPromise.then(function (results) {
console.log(results);
});
// [
// { status: 'fulfilled', value: 42 },
// { status: 'rejected', reason: -1 }
// ]
复制代码
Promise.allSettled()
方法接受一组 Promise
实例做为参数,返回一个新的 Promise 实例。fulfilled
仍是 rejected
,包装实例才会结束。Promise
实例,一旦结束,状态老是 fulfilled
,不会变成 rejected
。Promise
实例给监听函数传递一个数组 results
。该数组的每一个成员都是一个对象,对应传入 Promise.allSettled
的 Promise 实例。每一个对象都有 status 属性,对应着 fulfilled
和 rejected
。 fulfilled
时,对象有 value
属性, rejected
时有 reason
属性,对应两种状态的返回值。const formatSettledResult = (success, value) =>
success
? { status: "fulfilled", value }
: { status: "rejected", reason: value };
Promise.all_settled = function (iterators) {
const promises = Array.from(iterators);
const num = promises.length;
const resultList = new Array(num);
let resultNum = 0;
return new Promise((resolve) => {
promises.forEach((promise, index) => {
Promise.resolve(promise)
.then((value) => {
resultList[index] = formatSettledResult(true, value);
if (++resultNum === num) {
resolve(resultList);
}
})
.catch((error) => {
resultList[index] = formatSettledResult(false, error);
if (++resultNum === num) {
resolve(resultList);
}
});
});
});
};
const resolved = Promise.resolve(42);
const rejected = Promise.reject(-1);
Promise.all_settled([resolved, rejected]).then((results) => {
console.log(results);
});
复制代码
公司:快手
分类:JavaScript
跨域是针对浏览器的“同源策略”提出的说法。之因此有“同源策略”这种模式是基于网络安全方面的考虑。所谓的同源策略关注三点:
http:www.baidu.com & https.www.baidu.com http
协议不一样,跨域)https://www.aliyun.com & https://developer.aliyun.com
域名不一样,跨域)http://localhost:8080 & http://localhost:8000
端口号不一样,跨域)“同源策略”对于跨域网络资源的设定很是的清晰。
这些场景涉及到跨域禁止操做:
为何要阻止跨域呢?上文咱们说过是基于安全策略:好比一个恶意网站的页面经过 iframe 嵌入了银行的登陆页面(两者不一样源),若是没有同源限制,恶意网页上的 javascript 脚本就能够在用户登陆银行的时候获取用户名和密码。
针对跨越问题咱们该如何解决,主流的方案有如下:
一、 经过 jsonp 跨域 二、 document.domain + iframe 跨域 三、 location.hash + iframe 四、 window.name + iframe 跨域 五、 postMessage 跨域 六、 跨域资源共享(CORS) 七、 nginx 代理跨域 八、 nodejs 中间件代理跨域 九、 WebSocket 协议跨域
跨域并不是浏览器限制了发起跨站请求,而是跨站请求能够正常发起,可是返回结果被浏览器拦截了。
每次需求都会发出,服务器端也会作出响应,只是浏览器端在接受响应的时候会基于同源策略进行拦截。
注意:有些浏览器不容许从 HTTPS 的域跨域访问 HTTP,好比 Chrome 和 Firefox,这些浏览器在请求还未发出的时候就会拦截请求,这是一个特例。
公司:快手
分类:网络&安全
浏览器缓存主要分为四个阶段:
强缓存通常存放于 Memory Cache 或者 Disk Cache。
etag 能够经过文件的 Last-Modified 和 content-length 计算。
Nginx官方默认的ETag计算方式是为"文件最后修改时间16进制-文件长度16进制"。例:ETag: “59e72c84-2404”
注意:
无论怎么样的算法,在服务器端都要进行计算,计算就有开销,会带来性能损失。所以为了榨干这一点点性能,很多网站彻底把Etag禁用了(好比Yahoo!),这其实不符合HTTP/1.1的规定,由于HTTP/1.1老是鼓励服务器尽量的开启Etag。
不使用缓存常见的方法是经过 url 拼接 random 的方式或者设置 Cache-Control 设置 no-cache。
公司:快手
分类:其它
网页从加载到呈现会经历一系列过程,针对每一个过程进行优化
经过 performance.timing
API,能够获取各个阶段的执行时间:
{
navigationStart: 1578537857229; //上一个文档卸载(unload)结束时的时间戳
unloadEventStart: 1578537857497; //表征了unload事件抛出时的时间戳
unloadEventEnd: 1578537857497; //表征了unload事件处理完成时的时间戳
redirectStart: 0; // 重定向开始时的时间戳
redirectEnd: 0; //重定向完成时的时间戳
fetchStart: 1578537857232; //准备好HTTP请求来获取(fetch)文档的时间戳
domainLookupStart: 1578537857232; //域名查询开始的时间戳
domainLookupEnd: 1578537857232; //域名查询结束的时间戳
connectStart: 1578537857232; //HTTP请求开始向服务器发送时的时间戳
connectEnd: 1578537857232; //浏览器与服务器之间的链接创建时的时间戳
secureConnectionStart: 0; //安全连接的握手时的U时间戳
requestStart: 1578537857253; //HTTP请求(或从本地缓存读取)时的时间戳
responseStart: 1578537857491; //服务器收到(或从本地缓存读取)第一个字节时的时间戳。
responseEnd: 1578537857493; //响应结束
domLoading: 1578537857504; //DOM结构开始解析时的时间戳
domInteractive: 1578537858118; //DOM结构结束解析、开始加载内嵌资源时的时间戳
domContentLoadedEventStart: 1578537858118; //DOMContentLoaded 事件开始时间戳
domContentLoadedEventEnd: 1578537858118; //当全部须要当即执行的脚本已经被执行(不论执行顺序)时的时间戳
domComplete: 1578537858492; //当前文档解析完成的时间戳
loadEventStart: 1578537858492; //load事件被发送时的时间戳
loadEventEnd: 1578537858494; //当load事件结束时的时间戳
}
复制代码
主要是针对重定向、DNS、TCP 链接进行优化
dns-prefetch
,同时将同类型的资源放到一块儿,减小 domain
数量也是能够减小 DNS 查找 Keep-Alive
选项和服务器创建长链接,让多个资源经过一个 TCP 链接传输。减小浏览器向浏览器发送的请求数目以及请求资源的大小是请求优化的核心思想
1.3.1 页面加载的核心指标
>=50ms 的任务
1.4.1 首屏时间:
指用户打开网站开始,到浏览器首屏内容渲染完成的时间。对于用户体验来讲,首屏时间是用户对一个网站的重要体验因素。一般一个网站,若是首屏时间在 5 秒之内是比较优秀的,10 秒之内是能够接受的,10 秒以上就不可容忍了。超过 10 秒的首屏时间用户会选择刷新页面或马上离开。
1.4.2首屏时间计算:
一般适用于首屏内容不须要经过拉取数据才能生存以及页面不考虑图片等资源加载的状况,咱们会在 HTML 文档中对应首屏内容的标签结束位置,使用内联的 JavaScript 代码记录当前时间戳。以下所示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>首屏</title>
<script type="text/javascript"> window.pageStartTime = Date.now(); </script>
<link rel="stylesheet" href="common.css" />
<link rel="stylesheet" href="page.css" />
</head>
<body>
<!-- 首屏可见模块1 -->
<div class="module-1"></div>
<!-- 首屏可见模块2 -->
<div class="module-2"></div>
<script type="text/javascript"> window.firstScreen = Date.now(); </script>
<!-- 首屏不可见模块3 -->
<div class="module-3"></div>
<!-- 首屏不可见模块4 -->
<div class="module-4"></div>
</body>
</html>
复制代码
此时首屏时间等于 firstScreen - performance.timing.navigationStart
事实上首屏模块标签标记法 在业务中的状况比较少,大多数页面都须要经过接口拉取数据才能完整展现
一般咱们首屏内容加载最慢的就是图片资源,所以咱们会把首屏内加载最慢的图片的时间当作首屏的时间。
DOM 树构建完成后将会去遍历首屏内的全部图片标签,而且监听全部图片标签 onload 事件,最终遍历图片标签的加载时间的最大值,并用这个最大值减去 navigationStart
便可得到近似的首屏时间。
此时首屏时间等于 加载最慢的图片的时间点 - performance.timing.navigationStart
因为统计首屏内图片完成加载的时间比较复杂。所以咱们在业务中一般会经过自定义模块内容,来简化计算首屏时间。以下面的作法:
1.4.3首屏优化方案:
公司:快手
分类:JavaScript
要了解某个元素是否进入了"视口"(viewport),即用户能不能看到它,传统的实现方法是,监听到scroll
事件后,调用目标元素的getBoundingClientRect()
方法,获得它对应于视口左上角的坐标,再判断是否在视口以内。而后声明一个全局变量,每出现一次就加一,就能够得出在视口出现了几回。这种方法的缺点是,因为scroll
事件密集发生,计算量很大,容易形成性能问题。
因而便有了 IntersectionObserver API
var io = new IntersectionObserver(callback, option);
复制代码
上面代码中,IntersectionObserver
是浏览器原生提供的构造函数,接受两个参数:callback
是可见性变化时的回调函数,option
是配置对象(该参数可选)。
构造函数的返回值是一个观察器实例。实例的observe
方法能够指定观察哪一个 DOM 节点。
// 开始观察
io.observe(document.getElementById("example"));
// 中止观察
io.unobserve(element);
// 关闭观察器
io.disconnect();
复制代码
上面代码中,observe
的参数是一个 DOM 节点对象。若是要观察多个节点,就要屡次调用这个方法。
io.observe(elementA);
io.observe(elementB);
复制代码
目标元素的可见性变化时,就会调用观察器的回调函数callback
。
callback
通常会触发两次。一次是目标元素刚刚进入视口(开始可见),另外一次是彻底离开视口(开始不可见)。
var io = new IntersectionObserver((entries) => {
console.log(entries);
});
复制代码
callback
函数的参数(entries
)是一个数组,每一个成员都是一个IntersectionObserverEntry
对象。若是同时有两个被观察的对象的可见性发生变化,entries
数组就会有两个成员。
IntersectionObserverEntry
对象提供目标元素的信息,一共有六个属性。
{
time: 3893.92,
rootBounds: ClientRect {
bottom: 920,
height: 1024,
left: 0,
right: 1024,
top: 0,
width: 920
},
boundingClientRect: ClientRect {
// ...
},
intersectionRect: ClientRect {
// ...
},
intersectionRatio: 0.54,
target: element
}
复制代码
每一个属性的含义以下。
time
:可见性发生变化的时间,是一个高精度时间戳,单位为毫秒target
:被观察的目标元素,是一个 DOM 节点对象rootBounds
:根元素的矩形区域的信息,getBoundingClientRect()
方法的返回值,若是没有根元素(即直接相对于视口滚动),则返回null
boundingClientRect
:目标元素的矩形区域的信息intersectionRect
:目标元素与视口(或根元素)的交叉区域的信息intersectionRatio
:目标元素的可见比例,即intersectionRect
占boundingClientRect
的比例,彻底可见时为1
,彻底不可见时小于等于0
IntersectionObserver
构造函数的第二个参数是一个配置对象。它能够设置如下属性。
2.3.1 threshold 属性:
threshold
属性决定了何时触发回调函数。它是一个数组,每一个成员都是一个门槛值,默认为[0]
,即交叉比例(intersectionRatio
)达到0
时触发回调函数。
new IntersectionObserver(
(entries) => {
/* ... */
},
{
threshold: [0, 0.25, 0.5, 0.75, 1],
}
);
复制代码
用户能够自定义这个数组。好比,[0, 0.25, 0.5, 0.75, 1]
就表示当目标元素 0%、25%、50%、75%、100% 可见时,会触发回调函数。
2.3.2 root 属性、rootMargin 属性:
不少时候,目标元素不只会随着窗口滚动,还会在容器里面滚动(好比在iframe
窗口里滚动)。容器内滚动也会影响目标元素的可见性。
IntersectionObserver API 支持容器内滚动。root
属性指定目标元素所在的容器节点(即根元素)。注意,容器元素必须是目标元素的祖先节点。
var opts = {
root: document.querySelector(".container"),
rootMargin: "500px 0px",
};
var observer = new IntersectionObserver(callback, opts);
复制代码
上面代码中,除了root
属性,还有rootMargin属性。后者定义根元素的margin
,用来扩展或缩小rootBounds
这个矩形的大小,从而影响intersectionRect
交叉区域的大小。它使用 CSS 的定义方法,好比10px 20px 30px 40px
,表示 top、right、bottom 和 left 四个方向的值。
这样设置之后,无论是窗口滚动或者容器内滚动,只要目标元素可见性变化,都会触发观察器。
---------------------------------- 一条讲武德的分割线 ------------------------------
因掘金发文的字数限制,剩下的答案你们扫码便可查看。
var versions = ["1.45.0", "1.5", "6", "3.3.3.3.3.3.3"];
// 要求从小到大排序,注意'1.45'比'1.5'大
function sortVersion(versions) {
// TODO
}
// => ['1.5','1.45.0','3.3.3.3.3.3','6']
复制代码
公司:头条
分类:JavaScript
分类:Node
function repeat(func, times, wait) {
// TODO
}
const repeatFunc = repeat(alert, 4, 3000);
// 调用这个 repeatFunc ("hellworld"),会alert4次 helloworld, 每次间隔3秒
复制代码
公司:头条
分类:JavaScript
公司:玄武科技
分类:JavaScript
公司:玄武科技
分类:JavaScrip