0%

维护一个使用Backbone框架的前端项目近三个月了,也是因为这个项目开始了学习和研究Backbone。

Backbone诞生于2010年,目前看该框架有些old。Backbone更关注组件化作用域控制等方面,从前端技术发展趋势的角度而言,目前层出不穷的现代化前端框架的诞生,都可以认为是Angular和Backbone等古典前端框架设计思想走向融合之后的产物。

Backbone介绍:

Backbone.js为复杂WEB应用程序提供模型(model)、集合(collection)、视图(view)的结构。其中模型用于绑定键值数据和自定义事件;集合附有可枚举函数的丰富API; 视图可以声明事件处理函数,并通过RESRful JSON接口连接到应用程序。

通过Backbone,可以将数据呈现为 Models, 你可以对模型进行创建,验证和销毁,以及将它保存到服务器。 任何时候只要UI事件引起模型内的属性变化,模型会触发*”change”事件; 所有显示模型数据的 Views 会接收到该事件的通知,继而视图重新渲染。 你无需查找DOM来搜索指定id*的元素去手动更新HTML。 — 当模型改变了,视图便会自动变化。

Backbone依赖Underscore.js和jQuery。

Backbone文档:https://www.backbonejs.com.cn/

Backbone实践:

  1. 视图(View)

    Backbone 视图几乎约定比他们的代码多 — 他们并不限定你的HTML或CSS, 并可以配合使用任何JavaScript模板库( Mustache.js, Haml-js等)。通过绑定视图的 render 函数到模型的 "change" 事件 — 模型数据会即时的显示在 UI 中。

    // test_view.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    var TestModel = require('test_model.js');
    var TestView = Backbone.View.extend({
    tagname:'div',
    className: "test_content",
    template: _.template($('#template_test').html()),
    events: {
    "click .view_more": "viewMore",
    },
    initialize: function(options) {
    this.model = new TestModel();
    this.listenTo(this.model, 'change', this.render);
    },
    render: function() {
    var data = this.model.toJSON();
    this.$el.html(this.template(data));
    }
    viewMore: function() {
    this.model.set({showMore:true});
    }
    })
    module.exports = TestView;

    // test_template.haml

    1
    2
    3
    4
    5
    6
    7
    8
    %script#template_test type="text/template"
    %div.test_box
    %h3
    <%=title%>
    %div
    <%=content%>
    %a.view_more
    点击查看更多内容
  2. 模型(Models)

    Models(模型)是任何Javascript应用的核心,包括数据交互及与其相关的大量逻辑: 转换、验证、计算属性和访问控制。

    // test_model.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var TestModel = Backbone.Model.extend({
    idAttribute:'test_id',
    defaults: {
    title: '标题',
    content: '内容',
    showMore: false
    }
    })
    module.exports = TestModel;
  3. 集合(Collection)

    集合是模型的有序组合,可以在集合上绑定 "change" 事件,从而当集合中的模型发生变化时fetch(获得)通知,集合也可以监听 "add""remove" 事件, 从服务器更新,并能使用 Underscore.js 提供的方法。

    // test_collection.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    var TestModel = require('test_model.js');
    var TestCollection = Backbone.Collection.extend({
    model: new TestModel()
    });
    var TestGroup = new TestCollection([
    {
    title: "标题1",
    content: "内容1",
    showMore: false
    },
    {
    title: "标题2",
    content: "内容2",
    showMore: false
    }
    ])
    module.exports = TestGroup;

斐波那契数列:

0、1、1、2、3、5、8、13、21、34、55、89、144、233、377、610、987、1597、2584、4181、6765、10946、17711、28657、46368、75025、121393、196418、317811 ……

  • 2 是上两项的和(1+1)
  • 3 是上两项的和(1+2)
  • 5 是(2+3)
  • 依此类推!

规律:当前项的值是上两项的和,第n项的值等于Xn = X(n-1)+X(n+1)

求斐波那契数列中第n项的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
const fibonacci = (n: number) => {
let pre = 1,cur = 1;
if (n === 0) {
return 0;
}
if (n === 1 || n === 2) {
return 1;
}
for (let i = 3; i <= n; i++) {
[pre, cur] = [cur, pre + cur];
}
return cur;
};

一、sort排序

1
2
3
4
const arr = [20, 1, 6, 3, 47, 4]
arr.sort(); //[1, 20, 3, 4, 47, 6]

arr.sort((a,b)=>a-b) // [1, 3, 4, 6, 20, 47]

sort()方法默认是按照unicode编码来进行比较的,而不是按照我们想的比较两个数值间的大小来进行排序了。对Number类型的数组元素先调用toString()方法,再按照字符串的Unicode编码进行排序。

对象类型的数组根据对象中的某个属性进行排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const arr = [
{name:'zopp',age:1},
{name:'gpp',age:20},
{name:'yjj',age:6}
];

function compare(property){
return function(a,b){
var value1 = a[property];
var value2 = b[property];
return value1 - value2;
}
}
console.log(arr.sort(compare('age')))
//[
{name:'zopp',age:1},
{name:'yjj',age:6},
{name:'gpp',age:20},
];

二、使用 localecompare 对中文进行排序

在网页上展示列表时经常需要对列表进行排序:按照修改/访问时间排序、按照地区、按照名称排序。

JavaScript 提供本地化文字排序,比如对中文按照拼音排序,不需要程序显示比较字符串拼音。String.prototype.localeCompare在不考虑多音字的前提下,基本可以完美实现按照拼音排序。

汉语拼音声明表:b p m f d t n l g k h j q x zh ch sh r z s y w

1
2
3
4
string.localCompare(target, locals)

'北京'.localeCompare('上海', 'zh')// -1
'佛山'.localeCompare('北京', 'zh')// 1

localCompare是根据我们的中文系统,把汉字先转换成了拼音,再进行了比较;对于同拼音的汉字,js再根据声调进行比较。

数组中按对象的某个属性名按拼音进行排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const arr = [
{name:"北京",age:111},
{name:"上海",age:122},
{name:"广州",age:222}
]
const property ="name"
arr.sort((a,b)=> {
return a[property].localeCompare(b[property])
})

console.log(arr)
//const arr = [
{name:"北京",age:111},
{name:"广州",age:222},
{name:"上海",age:122}
]

为什么JavaScript是单线程?

  • 防止DOM渲染冲突的问题;
  • Html5中的Web Worker可以实现多线程

什么是任务队列?

任务队列”是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,”任务队列”上第一位的事件就自动进入主线程。

同步和异步任务

  1. 同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
  2. 异步任务指的是,不进入主线程、而进入”任务队列”(task queue)的任务,只有”任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

执行流程

  1. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
  2. 主线程之外,还存在一个”任务队列”(task queue)。只要异步任务有了运行结果,就在”任务队列”之中放置一个事件。
  3. 一旦”执行栈”中的所有同步任务执行完毕,系统就会读取”任务队列”,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
  4. 主线程不断重复上面的第三步。

什么是事件循环(EventLoop)?

主线程从”任务队列”中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。

Event Loop线程处理的任务被分为两类即 微任务(micro task)和宏任务(macro task) ,这两个任务分别维护一个队列,都是采用先进先出的策略进行执行。

宏任务:setTimeout、setInterval、I/O。

微任务:Promise.then、 MutationObserver。

当前栈执行完后,Event Loop线程首先会执行微任务队列中事件,然后执行宏任务队列的事件,如果这时发现了微任务,则执行微任务,然后执行宏任务,反复循环至所有事件执行完毕。微任务永远在宏任务之前执行。

函数节流和函数防抖都是在时间轴上控制函数的执行次数。

一、函数节流(throttle)

概念: 规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。

生活中的实例: 我们知道目前的一种说法是当 1 秒内连续播放 24 张以上的图片时,在人眼的视觉中就会形成一个连贯的动画,所以在电影的播放(以前是,现在不知道)中基本是以每秒 24 张的速度播放的,为什么不 100 张或更多是因为 24 张就可以满足人类视觉需求的时候,100 张就会显得很浪费资源。

这里以判断页面是否滚动到底部为例,普通的做法就是监听 window 对象的 scroll 事件,然后再函数体中写入判断是否滚动到底部的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
React.useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);

const handleScroll = () => {
// 判断是否滚动到底部的逻辑
let pageHeight = body.height(),
scrollTop = window.scrollTop(),
winHeight = window.height(),
thresold = pageHeight - scrollTop - winHeight;
if (thresold > -100 && thresold <= 20) {
console.log('end');
}
};

这样做的一个缺点就是比较消耗性能,因为当在滚动的时候,浏览器会无时不刻地在计算判断是否滚动到底部的逻辑,而在实际的场景中是不需要这么做的,在实际场景中可能是这样的:在滚动过程中,每隔一段时间在去计算这个判断逻辑。而函数节流所做的工作就是每隔一段时间去执行一次原本需要无时不刻地在执行的函数,所以在滚动事件中引入函数的节流是一个非常好的实践。

加上函数节流之后,当页面再滚动的时候,每隔 300ms 才会去执行一次判断逻辑。

简单来说,函数的节流就是通过闭包保存一个标记(canRun = true),在函数的开头判断这个标记是否为 true,如果为 true 的话就继续执行函数,否则则 return 掉,判断完标记后立即把这个标记设为 false,然后handleScroll函数的执行包在 setTimeout 中,最后在 setTimeout 执行完毕后再把标记设置为 true,表示可以执行下一次的循环了。当 setTimeout 还未执行的时候,canRun 这个标记始终为 false,在函数开始的判断中被 return 掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
React.useEffect(() => {
window.addEventListener("scroll", handleFun);
return () => window.removeEventListener("scroll", handleFun);
}, []);

let canRun = true;

const handleFun = () => {
return () => {
if (!canRun) {
return;
}
canRun = false;
setTimeout(() => {
handleScroll();
canRun = true;
}, 300);
}
}

const handleScroll = () => {
// 判断是否滚动到底部的逻辑
let pageHeight = body.height(),
scrollTop = window.scrollTop(),
winHeight = window.height(),
thresold = pageHeight - scrollTop - winHeight;
if (thresold > -100 && thresold <= 20) {
console.log('end');
}
};

二、函数防抖(debounce)

概念: 在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

生活中的实例: 如果有人进电梯(触发事件),那电梯将在10秒钟后出发(执行事件监听器),这时如果又有人进电梯了(在10秒内再次触发该事件),我们又得等10秒再出发(重新计时)。

这里以用户注册时验证用户名是否被占用为例,如今很多网站为了提高用户体验,不会在输入框失去焦点的时候再去判断用户名是否被占用,而是在输入的时候就在判断这个用户名是否已被注册。这样的做法不好的是当用户输入第一个字符的时候,就开始请求判断了,不仅对服务器的压力增大了,对用户体验也未必比原来的好。

而理想的做法应该是这样的,当用户输入第一个字符后的一段时间内如果还有字符输入的话,那就暂时不去请求判断用户名是否被占用。

在这里引入函数防抖就能很好地解决这个问题,其实函数防抖的原理也非常地简单,通过闭包保存一个标记来保存 setTimeout 返回的值,每当用户输入的时候把前一个 setTimeout clear 掉,然后又创建一个新的 setTimeout,这样就能保证输入字符后的 interval 间隔内如果还有字符输入的话,就不去请求判断用户名是否被占用了。

1
2
3
4
5
6
7
8
9
10
11
12
13
let timeout = null;

const checkNameFun = () {
return () => {
if(timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
//TO DO 调接口检查是否用户名是否存在
doCheckUserName(name)
}, 300);
};
}

相信每个前端同学对于this指向都有过探索,虽然平时的开发中极少使用this,但是踩过坑以后都会意识到this指向的重要性,笔试的时候出现率是极高的……

ES5中的this

一般的,this指向函数运行(调用)时所在的执行环境

  • 在普通函数中的this总是代表他的直接调用者,默认情况下指向windos
  • 在严格模式下,没有直接调用者的函数中的this是undefined使用
  • call,apply,bind,this指向的是绑定的对象
  • 在全局的调用函数里this指向window
  • 对象函数调用,那个函数调用this就指向那个对象
  • 定时器(setTimeout,setInterval)中的this指向全局变量对象
  • 构造函数中的this指向构造出来的新对象

例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var obj = {
b:"10",
n:"11",
a: function() {
console.log(this);
console.log(this.a);
console.log(this.n);
console.log(this.b);
}
}

var b = obj.a;
b();
// window
// undefined
// undefined
// f() {
console.log(this);
console.log(this.a);
console.log(this.n);
console.log(this.b);
}

分析代码:

1
2
3
4
5
6
7
var b = obj.a;    
var b = function() {
console.log(this);
console.log(this.a);
console.log(this.n);
console.log(this.b);
}

b()函数调用时,其所在的执行环境是全局环境,所以this指向全局变量对象window

例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function fun() {
console.log("id1:", this.id);
console.log("this1:", this);
setTimeout(function() {
console.log("id2:", this.id);
console.log("this2:", this);
}, 0);
}

var id = 10;

fun();
// id1: 10
// this1: window
// id2: 10
// this2: window


fun.call({id:20});
// id1: 20
// this1: {id:20}
// id2: 10
// this2: window

使用fun函数的call方法改变了fun函数调用时函数体内this的指向({id: 42}这个对象,但setTimeout回调函数中的this依旧指向window对象(因为setTimeout在全局环境中运行)。

ES6中的this

  • 箭头函数没有自己的this,它的this是继承来的,默认指向在定义它时所在的对象。即箭头函数中的this是指向外层代码块的this。

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function fun() {
console.log("id1:", this.id);
console.log("this1:", this);
setTimeout(() => {
console.log("id2:", this.id);
console.log("this2:", this);
}, 0);
}

var id = 10;

fun();

// Chrome
// id1: 10
// this1: window
// id2: 10
// this2: window

fun.call({id: 20});

// Chrome
// id1: 20
// this1: {id: 20}
// id2: 20
// this2: {id: 20}

因为箭头函数(setTimeout回调)没有自己的this,导致其内部的this引用了外层代码块的this,即fun函数的this。

因为箭头函数内部的this是指向外层代码块的this(最近的this foo函数)的,所以我们可以通过改变外层代码块的this的指向从而改变箭头函数中this的指向(使用fun函数的call方法)。

例子:

1
2
3
4
5
6
7
8
var id = 10;
let fun = () => {
console.log(this.id)
};
fun(); // 10
fun.call({ id: 20 }); // 10
fun.apply({ id: 20 }); // 10
fun.bind({ id: 20 })(); // 10

解释:箭头函数中的this指向外层代码块的this,该代码块的this是window,代码中fun是箭头函数,无法改变this指向,因为箭头函数本身没有this,所以打印的都是10。

用React的Component或PureComponent来创建类组件时都会自动调用shouldComponentUpdate()生命周期函数,但区别是什么?

区别:

使用Component组件时,当 props 或 state 发生变化时,shouldComponentUpdate() 会在渲染执行之前被调用,返回值默认为 true。不会进行比较来确定返回值是true或false,默认就返回true。

使用PureComponent组件时,当props或state发生变化时,PureComponent会对props和state进行浅比较来决定shouldComponentUpdate() 函数的返回值是true或false。

pureComponent的优缺点

优点: 不需要手写shouldComponentUpdate(),通过浅比较来提升性能,可以减少不必要的render。当组件更新时,PureComponent的shouldComponentUpdate()函数会对props和state做了一个浅对比,如果组件的state和prop都没有发生变化,就不会触发render方法,省去了virtual DOM的diff和重新生成的过程,从而提升了性能。

缺点:由于进行的是浅比较,可能由于深层的数据不一致导致而产生错误的否定判断,从而导致页 面得不到更新。不适合使用在含有多层嵌套对象的state和prop中。

React结合Redux时需要用到中间件connect,@connect是什么?

@connect是react中用装饰器来装饰connect的写法

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from 'react';
import * as actionCreators from './actionCreators';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';

function mapStateToProps(state) {
return { todos: state.todos };
}

function mapDispatchToProps(dispatch) {
return { actions: bindActionCreators(actionCreators, dispatch) };
}

class MyApp extends React.Component {
// ...define your main app here
}

export default connect(mapStateToProps, mapDispatchToProps)(MyApp);

使用装饰器的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import * as actionCreators from './actionCreators';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';

function mapStateToProps(state) {
return { todos: state.todos };
}

function mapDispatchToProps(dispatch) {
return { actions: bindActionCreators(actionCreators, dispatch) };
}

@connect(mapStateToProps, mapDispatchToProps)
export default class MyApp extends React.Component {
// ...define your main app here
}

但是由于装饰器的兼容性问题我们需要使用babel来转换,安装babel插件

1
yarn add @babel/plugin-proposal-decorators

package.json下的babel配置

1
2
3
4
5
6
7
8
9
10
11
12
13
"babel": {
"presets": [
"react-app"
],
"plugins": [
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
]
]
},

根目录下创建jsconfig.json解决编辑器报错问题

1
2
3
4
5
{
"compilerOptions": {
"experimentalDecorators": true
}
}

默认情况下,浏览器会记录用户网页上提交的输入框的信息。这使得浏览器能够提供自动补全(在用户开始输入的时候给用户提供可能的内容)和自动填充(在加载的时候预先填充某些字段)功能。

这些功能通常是默认启用的,但可能涉及用户的隐私,因此浏览器允许用户禁用这些功能。

<input>元素关闭自动填充效果可以使用autocomplete=“off”来实现。

在Ant Design中Input组件中设置autocomplete=“off”是无效的,需要c大写,写成autoComplete=“off”有效。

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
import React from "react";
import {Input} from "antd";

class Example extends React.Component {
render() {
return(
<div>
<input autocomplete="off"/>
<Input autoComplete="off"/>
</div>
)
}
}

今天入职新的公司,接手了一个新项目。git commit时遇到一个问题: husky > pre-commit (node v12.13.0) Stashing changes…,检查后了解到是项目中安装了husky。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ git commit -m "修改内容"
husky > pre-commit (node v12.16.1)
Stashing changes... [started]
Stashing changes... [skipped]
→ No partially staged files found...
Running linters... [started]
Running tasks for **/*.less [started]
Running tasks for **/*.{js,jsx} [started]
Running tasks for **/*.{js,ts,tsx,json,jsx,less} [started]
stylelint --syntax less [started]
npm run lint-staged:js [started]
node ./scripts/lint-prettier.js [started]
node ./scripts/lint-prettier.js [completed]
git add [started]
git add [completed]
Running tasks for **/*.{js,ts,tsx,json,jsx,less} [completed]
stylelint --syntax less [failed]

Running tasks for **/*.less [failed]

npm run lint-staged:js [failed]

Running tasks for **/*.{js,jsx} [failed]

Running linters... [failed]

解决方法:

  1. 执行npm run lint ,根据提示修改错误。

    1
    npm run lint
  2. 使用git commit -m “” –no-verify ,绕过lint的检查。

    1
    git commit -m "" --no-verify

git hooks

如同其他许多的版本控制系统一样,Git 也具有在特定事件发生之前或之后执行特定脚本代码功能。

在git中提供了hook,就是在触发代码提交,push等一系列操作的时候,提供了触发其他程序的钩子.

hooks默认路径是 .git/hooks

husky

husky是一个npm包,安装后,可以很方便的在package.json配置git hook 脚本 。

1
2
3
4
5
6
7
8
9
10
// package.json 

"scripts": {
"lint": "eslint src"
},
"husky": {
"hooks": {
"pre-commit": "npm run lint"
}
},

设置后,在每一次git commit 之前,都会执行一次对应的 hook 脚本npm run lint

在安装 husky 的时候,husky会根据 package.json里的配置,在.git/hooks 目录生成所有的 hook 脚本(如果你已经自定义了一个hook脚本,husky不会覆盖它)。然后根据package.json里的设置,执行对应的hook脚本。