React 生命周期的打怪升级之路

React 生命周期的打怪升级之路

号外号外!走过路过千万不要错过!

截止目前为止 React 已经发布了 v16.12.0 版本, React 生命周期也是日常开发低头不见,抬头见的狗子,可惜狗子它变了。

改变原因

v16.3 版本之前, React 中的更新操作是同步的,这可能会导致性能问题。

举个例子,假如有一个庞大的模块里面嵌套超级多的组件,一旦最顶部的 render 方法执行了,然后依次执行组件的 render 方法,直到最底层组件。这个过程会导致主线程卡主。

官方为了解决这个问题,因此引入了 React Fiber,其解决思路是分片执行,一个更新过程被分为两个阶段(Phase):第一个阶段 Reconciliation Phase 和第二阶段 Commit Phase。

在第一阶段 Reconciliation Phase,React Fiber 会找出需要更新哪些 DOM,这个阶段是可以被打断的;但是到了第二阶段 Commit Phase,那就一鼓作气把 DOM 更新完,绝不会被打断。

而这两个阶段也对应到不同的生命周期:

第一阶段

  • componentWillMount
  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate

第二阶段

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

可以看看这个例子:Fiber vs Stack Demo

变更对比

以前:

现在( v16.3 ):

对比上下两张图,发现 React 废弃了以下方法:

  • componentWillMount
  • componentWillReceiveProps
  • componentWillUpdate

这里需要说明一下:为了做到版本版本兼容 增加 UNSAFE_componentWillMount,UNSAFE_componentWillReceivePropsUNSAFE_componentWillUpdate方法,新旧方法都能使用,但使用旧方法,开发模式下会有红色警告,在 React v17 更新时会彻底废弃。

新增了方法如下:

  • getDerivedStateFromProps
  • getSnapshotBeforeUpdate

阶段梳理

下面从三个阶段(挂载、更新、卸载)梳理下生命周期方法。

constructor 构造函数

执行的生命周期方法,如果需要做一些初始化操作,比如初始化 state, 反之则无需为 React 组件实现构造函数。

getDerivedStateFromProps

当组件实例化的时候,这个方法替代了 componentWillMount(),而当接收到新的 props 时,该方法替代了 componentWillReceiveProps() 和 componentWillUpdate()。

1
static getDerivedStateFromProps(nextProps, prevState)

其中 v16.3 版本中 re-rendering 之后此方法不会被调用,而 v16.4 版本中 re-rendering 之后都会调用此方法,这意味即使 props 未发生改变,一旦父组件发生 re-rendering 那么子组件的该方法依然会被调用。

componentWillMount/UNSAVE_componentWillMount (即将废弃)

部分同学日常会把数据请求放在该方法内,以便于快速获取数据并展现,但事实上,请求再快再怎么快,也快不过首次 render,并且 React Fiber 执行机制的原因,会导致该方法被执行多次,这也意味着接口被请求多次。因此该方法在 v17 版本以后将被彻底废弃。

componentDidMount

在组件挂载完成后调用,且全局只调用一次。可以在这里使用 refs,获取真实 dom 元素。该钩子内也可以发起异步请求,并在异步请求中可以进行 setState。

componentWillReceiveProps/UNSAFE_componentWillReceiveProps (即将废弃)

被 getDerivedStateFromProps 方法取代。

shouldComponentUpdate

每次调用 setState 后都会调用 shouldComponentUpdate 判断是否需要重新渲染组件。默认返回 true,需要重新 render。返回 false 则不触发渲染。在比较复杂的应用里,有一些数据的改变并不影响界面展示,可以在这里做判断,优化渲染效率。

componentWillUpdate

依旧是 React Fiber 执行机制的原因,在该方法记录 DOM 状态就不再准确了。

getSnapshotBeforeUpdate

触发该方法的时机,是在更新 DOM 之前的一瞬间,比 componentWillUpdate 记录的 DOM 状态更为精确。

componentDidUpdate

除了首次 render 之后调用 componentDidMount,其它 render 结束之后都是调用 componentDidUpdate。

componentWillUnmount

组件被卸载的时候调用。一般在 componentDidMount 里面注册的事件需要在这里删除。

总结

由于 React 同步更新组件的原因,会引起性能问题,造成主线程卡死,因此引入 React Fiber 对核心算法的一次重新实现。 紧接着发现, React Fiber 会让部分生命周期方法行多次,而废除这部分方法,引入新方法。

参考文章:

React Fiber 是什么

React v16.3 之后的组件生命周期函数

浅谈 React Fiber 及其对 lifecycles 造成的影响

讲讲今后 React 异步渲染带来的生命周期变化

React 新旧生命周期的思考理解

React Hooks学习笔记

React Hooks 学习笔记

看了一篇文章,也说清了 React 发展历程,我一直都坚信任何技术方案的出现都是为了以更好的方式解决问题。

React 组件发展历程

React 组件基本可以归结三个阶段:

  • createClass Components
  • Class Components
  • Function Components

createClass Components

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
const Index = React.createClass({
getDefaultProps() {
return {};
},
getInitialState() {
return {
name: "robin"
};
},
render() {
return <div></div>;
}
});

这是早期创建组建的方式,我想当时 JavaScript 还没有内置的类系统。当 ES6 到来的时候,这一切都改变了。

Class Components

示例:

1
2
3
4
5
6
7
8
9
10
11
12
class Index extends React.Component {
constructor(props) {
super(props);
this.state = {
name: "robin"
};
}

render() {
return <div></div>;
}
}

初始化属性及状态的方式随之发生了变化,这里或许会有疑问,createClass 用的好好地,为什么还要使用以 Class 继承的方式编写组件,我想应该是为了迎合 ES 的发展。

随着项目的的复杂度提升,如果项目中全部使用类的方式开发组件是不是有点重,这个时候它来了。

Function Components

示例:

1
2
3
const Button = props => {
return <div></div>;
};

我只是要封装个公共组件,用它就够。

夜黑风高夜,组件开发时……

想这种复合型组件就需要状态的介入,但是又犯不着使用类组件,因此诞生了 React Hooks

React Hooks 介绍

  • useState()
  • useContext()
  • useReducer()
  • useEffect()

useState

如果需要操作状态,代码如下:

1
2
3
4
5
6
7
import React, { useState } from "react";

export default function TextInput() {
const [name, setName] = useState("");

return <input value={name} onChange={e => setName(e.target.value)} />;
}

useContext

如果需要共享状态,代码如下:

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
31
32
const AppContext = React.createContext({});

const Button1 = () => {
const { name } = useContext(AppContext);
return <div>{name}</div>;
};
const Button2 = () => {
const { name, age } = useContext(AppContext);

return (
<div>
{name}
{age}
</div>
);
};

const Button3 = () => {
const [name, setName] = useState("robin111");
return (
<AppContext.Provider
value={{
name: name,
age: 29
}}
>
<Button1 />
<Button2 />
<button onClick={() => setName("test")}>211</button>
</AppContext.Provider>
);
};

当共享状态组件嵌套时,当前的 context 值由上层组件中距离当前组件最近的 <AppContext.Provider> 的 value prop 决定。

useReducer

Redux 提供了状态管理方案,在这里你也可以使用 useReducer。

1
const [state, dispatch] = useReducer(reducer, initialArg, init);

借鉴了阮一峰老师的计数器实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const myReducer = (state, action) => {
switch (action.type) {
case "countUp":
return {
...state,
count: state.count + 1
};
default:
return state;
}
};

function App() {
const [state, dispatch] = useReducer(myReducer, { count: 0 });
return (
<div className="App">
<button onClick={() => dispatch({ type: "countUp" })}>+1</button>
<p>Count: {state.count}</p>
</div>
);
}

useEffect

官方描述:Effect Hook 可以让你在函数组件中执行副作用操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, { useState, useEffect } from "react";

function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}

这段代码是官方提供的,简洁明了。

官方在 Effect 方面介绍的很详细,强烈推荐大家详读。中文社区详解

自定义 Hooks

自定义的目的,是为了封装重复逻辑,共享逻辑,既然是自定义 Hooks,也不是简单函数封装,当然被封装的逻辑代码中也需要包含官方提供的 Hook 方案,方能凑效。中文社区详解

总结

刚开始在项目中使用,还未能深入使用,只能列举目前能用到的示例代码,自定义 Hooks,在我看来是对 Hooks 的组合封装。

参考文章:

高阶组件

首次使用场景

项目初期只有一个添加商品模块,因业务迭代,从多图商品中分离出视频商品,当前存在的问题:

  1. 版本迭代势必同时修改两个模块;
  2. 大量逻辑,方法重复。

方案筛选

鉴于以上问题,寻找的解决方案有:

  1. 公共逻辑代码抽离;
  2. 公共容器组件抽离;
  3. Mixin;
  4. 高阶组件。

公共逻辑抽离方案,公共逻辑中包含了 state、props、以及 setState,如果提取公共逻辑代码,势必重新处理相关状态,对原有模块破坏性大。

公共容器组件方案,合并两个模块公共部分,把上传部分分离成多图上传组件,视频上传组件,通过传入参数区分类型,也就意味着合并路由名称,添加参数区分,修改成本高。

Mixin 方案,但其方案存在缺陷。详情,看这里

最后能选择的最优方案就是高阶组件,侵入性小,函数式思想(同样的输入,返回同样的输出、可预测性强),

概念介绍

高阶函数

其满足高阶函数的至少两个条件:

  • 接受一个或多个函数作为输入
  • 输出一个函数

高阶组件

定义:类比高阶函数的定义,高阶组件就是接受一个组件作为参数,在函数中对组件做一系列的处理,随后返回一个新的组件作为返回值。

高阶组件解读

示例 1 (属性代理模式)

1
2
3
4
5
6
7
const HOC = (WrappedComponent) => {
return class extends Component {
render() {
return <WrappedComponent {...this.props}/>
}
}
}

示例 2 (反向继承模式)

1
2
3
4
5
6
7
const HOC = (WrappedComponent) => {
return class extends WrappedComponent {
render() {
return super.render();
}
}
}

what? 属性代理、反向继承,继承就算了,怎么还反向继承,别急往下看

属性代理

简单讲就是包裹组件,操作 props,基本上这种组件嵌套的模式以及传参方式经常用到。

上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const HOC = (WrappedComponent) => {
return class extends Component {
render() {
return <WrappedComponent {...this.props}/>
}
}
}

class WrappedComponent extends Component{
render(){
//....
}
}

//高阶组件使用
export default HOC(WrappedComponent)

属性操作

上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const HOC = (WrappedComponent) => {
return class extends Component {
render() {
const newProps = {
name: "HOC"
}
return <WrappedComponent {...this.props} {...newProps}/>
}
}
}

class WrappedComponent extends Component{
render(){
//....
}
}

//高阶组件使用
export default HOC(WrappedComponent)

组件传参基本都干过

refs 引用

1
2
3
4
5
6
7
const HOC = (WrappedComponent) => {
return class extends Component {
render() {
return <WrappedComponent ref={this.onRef} {...this.props}/>
}
}
}

基本上都这个干过,使用组件实例方法。

抽象 State

上代码

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
const HOC = (WrappedComponent) => {
return class extends Component {
onChange = (data = {}) => {
this.setState(data)
}
render() {
const {name = ''} = this.state;
const newProps = {
name: {
value: name,
onChange: (e)=>this.onChange({name: e.target.value})
}
}
return <WrappedComponent {...this.props} {...newProps}/>
}
}
}

class WrappedComponent extends Component{
render(){
const {name} = this.props;
return <input {...name} />
}
}

//高阶组件使用
export default HOC(WrappedComponent)

细细品味上面写法的精髓,当你需要操作大量表单的时候,会发现它的好。

刚开始没理解抽象 State 的含义,通过写笔记搞明白了,就是把需要在原组件,通过 state 动态赋值的操作,抽象到高阶组件中通过 props 传值。

总结: 属性代理的模式,跟平常写组件的模式差不多,只不过加了些技巧。

反向继承

再看下面的示例

1
2
3
4
5
6
7
const HOC = (WrappedComponent) => {
return class extends WrappedComponent {
render() {
return super.render();
}
}
}

super.render()这种模式只在 es6 类继承的时候通过super.method(),用在这里实在是妙。

从写法上看,继承了传入组件,使用super.render()自然是执行了传入组件的 render 方法,也就是渲染了传入组件对应的页面,有趣的事情开始了。

操作 state 以及 props。

操作 state我理解,但操作 props,什么鬼,直到看到这样一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//该例子来源于 React 高阶组件(HOC)入门指南 掘金
const HOCFactoryFactory = (...params) => {
// 可以做一些改变 params 的事
return (WrappedComponent) => {
return class HOC extends Component {
render() {
return <WrappedComponent {...this.props} />;
}
}
}
}

HOCFactoryFactory(params)(WrappedComponent)

//类似场景 redux
connect(params)(Index)

又明白了。

渲染劫持

别人写的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//例子来源于《深入React技术栈》

const HOC = (WrappedComponent) =>
class extends WrappedComponent {
render() {
const elementsTree = super.render();
let newProps = {};
if (elementsTree && elementsTree.type === 'input') {
newProps = {value: 'may the force be with you'};
}
const props = Object.assign({}, elementsTree.props, newProps);
const newElementsTree = React.cloneElement(elementsTree, props, elementsTree.props.children);
return newElementsTree;
}
}
class WrappedComponent extends Component{
render(){
return(
<input value={'Hello World'} />
)
}
}
export default HOC(WrappedComponent)

跟操作 DOM 差不多(个人理解),目前没有用到这个的场景,不做赘述。

高阶组件基本使用场景介绍完了,回到正题,聊聊,我在实际项目中是怎么用的。

说几个数据:

改造模块前

多图商品模块代码,900 行。视频商品模块代码,1000 行

改造模块后

多图商品模块代码,554 行。视频商品模块代码,650 行

抽离出的高阶组件代码 387 行

当前水平只能到这一步了。

我采用了反向继承的方案,属性代理,只能满足操作 props,但不能操作 state,大家或许会有疑问,人家不是可以抽象 state,是的,瞅清楚,是抽象不是操作。

还有就是属性代理没办法使用原组件静态方法。大神提供了批量复制静态方法的库hoist-non-react-statics

最终满足我操作 props,尤其是操作 state 的可行性方案,就是高阶组件的反向继承。

目前研究学习并实践的内容就到这里了,如果后续有补充的,也会持续更新。

新起点-2020

image

2019过去了,也需要总结过去这一年的点点滴滴。

2019年的头等大事,当然是和我老婆结婚了,依稀记得,结婚现场我老婆没哭,而我却从头哭到尾……

在项目开发中收获颇多:

  • 推荐使用RN作为公司核心业务开发方案;
  • 搭建基于gitlab的CI/CD持续集成H5(测试、生产)Docker环境;
  • 搭建基于Jenkins的App端测试包输出环境;
  • 自建node服务,为公司内部提供,应用Icon批量生产服务、收款码批量生产服务、验证码查询服务;
  • 前端管理的第一年;

不足

  • 编码规范实践不到位;
  • 技术深度不足;
  • 管理方法不足;
  • 自定学习计划,未能完成。

19年,我焦虑的一年,不知道该怎么学,层出不穷的框架,真的让人眼花缭乱,不知道是以基础javascript为切入点,还是深入学习一门框架,很纠结。直到年底,算是坚定了学习的方向,以框架为主,学习框架实现原理,技巧,然后学习深入框架中使用的原生JS。

19年,也是我明白人各有志的一年,不是每个人都愿意主动学习,优化项目的方方面面,但我一定会发现问题、解决问题。

19年,也是我理解产品的一年,从刚开始的产品改需求,气的暴跳如雷,到耐心听产品,一遍一遍把项目优化完善。

19年,看到大佬更新的一篇篇blog,告诉我们一个道理,做到绝对的执行力,学习力,你也成为大佬。

20年,我也立个flag:

  • 一直以来都喜欢闺女,这一年一定完成生闺女的目标;
  • 每周更新一篇blog,总结当周学习内容;
  • 深挖React技术栈,从最基础的数据流、生命周期、组件,到目前学习的高阶组件,Hook,再到发现新大陆;
  • 学习swift,找一个开源项目,做一个Demo,为跨端开发做准备。
  • 对于170斤的我,是该减减肥了,那就先减个20斤……

前端进阶系列—JS执行机制


一直以来,对JS的执行机制都是模棱两可,知道今天看了文章—《这一次,彻底弄懂JavaScript执行机制》《Event Loop的规范和实现》,才对JS的执行机制有了深入的理解,下面是我的学习总结。

2个要点

  • JS是单线程语言
  • Event Loop是JS的执行机制,为了实现主线程的不阻塞,Event Loop就这么诞生了。

2个概念(结合Browser环境和Node环境)

  • task queue(宏任务队列):setTimeoutsetIntervalsetImmediateI/OUI交互事件
  • microtask queue(微任务队列):Promiseprocess.nextTickMutaionObserver

看下图:

  • queue可以看成一种数据结构,用以存储需要执行的函数
  • setTimeout等API注册的函数,会进入task队列
  • Promise等API注册的函数会进入microtask队列
  • Event Loop执行一次,从task队列中拉出一个task执行
  • Event Loop继续检查microtask队列是否为空,依次执行直至清空队列

情景再现

JS的执行逻辑,就好比只有一个窗口的银行,客户需要一个一个排队办理业务,假如现在排队的有两个人,第一个人是办理银行卡的,第二个人是取钱的,下面来个情景对话(这就类似上图的event loop):
客服:请问您办理什么业务?
客户1:办理银行卡。
客服:请先填写一份申请表。下一位!(此时客户1进入callback queue)
客服:请问您办理什么业务?
客户2:取钱
…………
(此时客户1已经完成申请表填写,但客户2还未结束,那么客户1还需等待,直到窗口前的这个客户办理结束)
客户1:我填好了,给您……

## 实例练习1

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
console.log(1)

setTimeout(() => {
console.log(2)
new Promise(resolve => {
console.log(4)
resolve()
}).then(() => {
console.log(5)
})
})

new Promise(resolve => {
console.log(7)
resolve()
}).then(() => {
console.log(8)
})

setTimeout(() => {
console.log(9)
new Promise(resolve => {
console.log(11)
resolve()
}).then(() => {
console.log(12)
})
})

分解动作:

  1. 主进程运行的代码首先输出1、7;
  2. 再执行一次microtask输出8;
  3. 执行了一次task输出2,4;
  4. 再执行一次microtask输出5;
  5. 再执行另一个task输出9、11;
  6. 再执行一次microtask输出12

最终结果:
1、7、8、2、4、5、9、11、12

注意的:在Node环境下process.nextTick注册的函数优先级高于Promise,大家可以在Node环境下尝试下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
new Promise(resolve => {
console.log(1)
resolve()
}).then(() => {
console.log(2)
})
new Promise(resolve => {
console.log(3)
resolve()
}).then(() => {
console.log(4)
})
process.nextTick(() => {
console.log(5)
})

console.log(6)

执行结果:1、3、6、5、2、4

实例练习2

1
2
3
4
5
6
7
8
9
10
11
setTimeout(() => {
console.log(2)
}, 2)

setTimeout(() => {
console.log(1)
}, 1)

setTimeout(() => {
console.log(0)
}, 0)

有人说结果应该是0,1,2
但正确结果是2,1,0。
因为setTimeout最低延迟是4ms,值得注意。

参考文章:

1
2
3
4
5
6
原文:Event Loop的规范和实现
地址:https://juejin.im/post/5a6155126fb9a01cb64edb45
原文:这一次,彻底弄懂 JavaScript 执行机制
地址:https://juejin.im/post/59e85eebf265da430d571f89
原文:JavaScript 运行机制详解:再谈Event Loop
地址:http://www.ruanyifeng.com/blog/2014/10/event-loop.html

前端进阶系列—什么是执行上下文?什么是调用栈?

1
2
原文作者:Valentino
原文链接:https://www.valentinog.com/blog/js-execution-context-call-stack

什么是Javascript中的执行上下文?

我打赌你不知道答案。

编程语言中最基础的组成部分是什么?

变量和函数对吗?每个人都可以学习这些板块。

但除了基础知识之外还有什么?

在称自己为中级(甚至是高级)Javascript开发人员之前,你应该掌握的Javascript的核心是什么?

有很多:Scope(作用域)Closure(闭包)Callbacks(回调)Prototype(原型)等等。

但在深入研究这些概念之前,您至少应该了解Javascript引擎的工作原理。

在这篇文章中,我们将介绍每个Javascript引擎的两个基本部分:执行上下文和调用堆栈。

(不要害怕。它比你想象的容易)。

准备好了吗?

目录

  1. 你会学到什么?
  2. Javascript如何执行您的代码?
  3. Javascript引擎
  4. 它是如何工作的?
  5. 全局存储器?
  6. 什么是调用栈?
  7. 什么是局部执行上下文?
  8. 总结

你会学到什么?

在这篇文章中你将学到:

  • Javascript引擎是如何工作的?
  • Javascript中执行上下文
  • 什么是调用栈
  • 全局执行上下文和局部执行上下文之间的区别

Javascript如何执行您的代码?

通过查看Javascript内部功能,您将成为更好的Javascript开发人员,即使您无法掌握每一个细节。

现在,看看下面的代码:

1
2
3
4
5
var num = 2;

function pow(num) {
return num * num;
}

现在告诉我:你认为在浏览器里以何种顺序执行这段代码?

换句话说,如果您是浏览器,您将如何阅读该代码?

这听起来很简单。

大多数人认为“是的,浏览器执行功能pow并返回结果,然后将2分配给num。”

在接下来的部分中,您将发现那些看似简单的代码行背后的机制。

Javascript引擎

要了解Javascript如何运行您的代码,我们应该遇到第一件可怕的事情:

执行上下文

在Javascript中什么是执行上下文?

每次在浏览器(或Node)中运行Javascript时,引擎都会执行一系列步骤。

其中一个步骤涉及创建全局执行上下文。

什么是引擎?

也就是说,Javascript引擎是运行Javascript代码的“引擎”。

如今有两个突出的Javascript引擎:Google V8和SpiderMonkey。

V8是Google开源的Javascript引擎,在Google Chrome和Nodejs中使用。

SpiderMonkey是Mozilla的JavaScript引擎,用于Firefox。

到目前为止,我们有Javascript引擎和执行上下文。

现在是时候了解它们如何协同工作了。

它是如何工作的?

每次运行一些Javascript代码是,引擎都会创造一个全局执行上下文。

执行上下文是一个比喻的词,用于描述运行Javascript代码的环境。

我觉得你很难想象出这些抽象的东西。

现在将全局执行上下文视为一个框:

让我们再看看我们的代码:

1
2
3
4
5
var num = 2;

function pow(num) {
return num * num;
}

引擎如何读取该代码?

这是一个简化版本:

引擎:第一行,它是变量!让我们将它存储在全局存储器中。

引擎:第三行,我看到了一个函数声明。让我们也把它存储在全局存储器中。

引擎:看起来我已经完成了。

如果我再次问你:浏览器如何“看到”以下代码,你会怎么说?

是的,它有点自上而下……

正如你所看到的那样,引擎没有运行功能pow!

这是一个函数声明,而不是函数调用。

上面的代码将转换为存储在全局存储器中的一些值:函数声明和变量。

全局存储器?

我已经对执行上下文感到困惑,现在还要问我什么是全局存储器?

接下来让我们看看什么是全局存储器

全局存储器

Javascript引擎也有一个全局存储器。

全局内存包含全局变量和函数声明供以后使用。

如果您阅读Kyle Simpson的“作用域和闭包”,您可能会发现全局存储器与全局作用域的概念重叠。

实际上它们是一回事。

这是些很难得概念。

但你现在不应该担心。

我希望你能理解我们难题的两个重要部分。

当Javascript引擎运行您的代码时,它会创建:

  • 全局执行上下文
  • 全局存储器(也称为全局作用域或全局变量环境)

一切都清楚了吗?

如果我在这一点上,我会:

  • 写下一些Javascript代码
  • 当你是引擎时,一步一步地解析代码
  • 在执行期间创建全局执行上下文和全局存储器的图形表示

您可以在纸上或使用原型制作工具编写练习。

对于我的小例子,图片看起来如下:

在下一节中,我们将看另一个可怕的事情:调用栈。

什么是调用栈?

您是否清楚地了解了执行上下文,全局存储器和Javascript引擎如何组合在一起?

如果没有,花时间查看上一节。

我们将在我们的难题中介绍另一篇文章:调用栈。

让我们首先回顾一下Javascript引擎运行代码时会发生什么。 它创建:

  • 全局执行上下文
  • 全局存储器

除了我们的例子,没有更多的事情发生:

1
2
3
4
5
var num = 2;

function pow(num) {
return num * num;
}

代码是纯粹的值分配。

让我们更进一步。

如果我调用该函数会发生什么?

1
2
3
4
5
6
7
var num = 2;

function pow(num) {
return num * num;
}

var res = pow(num)

有趣的问题。

在Javascript中调用函数的行为使引擎寻求帮助。

这个帮助来自Javascript引擎的朋友:调用栈。

它听起来可能并不明显,但Javascript引擎需要跟踪发生的情况。

它依赖于调用栈。

什么是Javascript中的调用栈?

调用栈就像是程序当前执行的日志。

实际上它是一个数据结构:堆栈。

调用栈的工作原理是什么?

不出所料,它有两种方法:push和pop。

push是将某些东西放入堆栈的行为。

也就是说,当您在Javascript中运行函数时,引擎会将该函数push到调用堆栈中。

每个函数调用都被push到调用栈中。

push的第一件事是main()(或global()),它是Javascript程序执行的主要线程。

现在,上一张图片看起来像这样:

pop另一端是从堆栈中删除某些东西的行为。

当函数执行结束时,将从调用栈中pop出去。

我们的调用栈将如下所示:

现在?您已准备好从那里掌握每个Javascript概念。

请看下一部分。

局部执行上下文

到目前为止,一切似乎都很清楚。

我们知道Javascript引擎创建了一个全局执行上下文和一个全局存储器。

然后,当您在代码中调用函数时:

  • Javascript引擎请求帮助
  • 这个帮助来自Javascript引擎的朋友:调用栈
  • 调用栈会跟踪代码中调用的函数

当你在Javascript中运行一个函数时,还有另一件事情发生。

首先,该功能出现在全局执行上下文中。

然后,另一个迷你上下文出现在函数旁边:

那个迷你上下文叫做局部执行上下文。

如果您注意到,在上一张图片中,全局存储器中会出现一个新变量:var res。

变量res的值首先是undefined。

然后,只要pow出现在全局执行上下文中,该函数就会执行并且res将获取其返回值。

在执行阶段,创建局部执行上下文以保存局部变量。

记住这一点。

了解全局和局部执行上下文是掌握作用域和闭包的关键。

总结

Javascript引擎创建执行上下文,全局存储器和调用栈。但是一旦你调用一个函数,引擎就会创建一个局部执行上下文。

经常被忽视的是,新的开发人员总是将Javascript内部视为神秘的东西。

然而,它们是掌握高级Javascript概念的关键。

如果你学习执行上下文,全局存储器和调用栈,那么Scope,Closures,Callbacks和其他东西将变得轻而易举。

特别是,理解调用堆栈是至关重要的。

一旦你想象它,所有的Javascript将开始有意义:你将最终理解为什么Javascript是异步的以及我们为什么需要回调。

前端进阶系列—盒模型

盒模型是界面布局需要掌握的基本功。

盒模型基本概念

盒模型四要素:marginborderpaddingcontent

盒模型分为:标准盒模型(W3C盒模型) 、 怪异盒模型(IE盒模型)

盒模型区别

怪异盒模型总宽度 = content + padding

标准盒模型总宽度 = content

盒模型使用

1
box-sizing: border-box(怪异盒模型) || content-box(标准盒模型)

兼容性

IE8及以上版本支持该属性,Firefox 需要加上浏览器厂商前缀-moz-,对于低版本的IOS和Android浏览器也需要加上-webkit-。

前端进阶系列—flex布局

以下内容主要摘抄自阮一峰老师的博客http://www.ruanyifeng.com/blog/2015/07/flex-grammar.html

背景

Flexbox布局(Flexible Box)模块(目前是W3C Last Call Working Draft)旨在提供更有效的布局方式,即使容器中的项目之间对齐和分配空间的大小未知或动态(因此单词“flex”)。

flex布局背后的主要思想是让容器能够改变其项目的宽度/高度(和顺序),以最好地填充可用空间(主要是为了适应所有类型的显示设备和屏幕尺寸)。Flex容器扩展项目以填充可用空间,或缩小它们以防止溢出。

最重要的是,flexbox布局与方向无关,而不是常规布局(基于垂直的块和基于水平的内联块)。虽然那些适用于页面,但它们缺乏灵活性来支持大型或复杂的应用程序(特别是在方向更改,调整大小,拉伸,缩小等方面)。

注意: Flexbox布局最适合应用程序的组件和小规模布局,而Grid布局则适用于更大规模的布局。

基本概念

容器默认存在两根轴:水平的主轴(main axis)和垂直的交叉轴(cross axis)。主轴的开始位置(与侧边的交叉点)叫做main-start,结束位置叫main end;交叉轴的开始位置叫cross start,结束位置叫cross end

项目默认沿主轴排列。单个项目占据的主轴空间叫main size,占据的交叉轴空间叫cross size

容器属性

  • flex-direction
  • flex-wrap
  • flex-flow
  • justify-content
  • align-items
  • align-content

    flex-direction

flex-direction属性决定主轴的方向(即项目的排列方向)。

1
2
3
.container {
flex-direction: row | row-reverse | column | column-reverse;
}
  • row (默认值):主轴为水平方向,起点在左端。
  • row-reverse:主轴为水平方向,起点在右端。
  • column:主轴为垂直方向,起点在上沿。
  • column-reverse:主轴为垂直方向,起点在下沿。

flex-wrap


默认情况下,flex项目都会尝试适合一行。您可以更改它并允许项目根据需要使用此属性进行换行。

1
2
3
.container{
flex-wrap: nowrap | wrap | wrap-reverse;
}
  • nowrap (默认值):不换行。
  • wrap:f换行,第一行在上方。
  • wrap-reverse:换行,第一行在下方。

flex-flow

flex-flow属性是flex-directionflex-wrap属性的复合属性。

flex-flow 属性用于设置或检索弹性盒模型对象的子元素排列方式。

1
flex-flow: <‘flex-direction’> || <‘flex-wrap’>

justify-content

justify-content属性定义了项目在主轴上的对齐方式

1
2
3
.container {
justify-content: flex-start | flex-end | center | space-between | space-around | space-evenly;
}
  • flex-start:默认值。项目位于容器的开头。
  • flex-end:项目位于容器的尾部。
  • center: 项目位于容器的中心。
  • space-between:两端对齐,项目之间的间隔都相等。
  • space-around:每个项目两侧的间隔相等。所以,项目之间的间隔比项目与边框的间隔大一倍。
  • space-evenly:使得位于容器内部任何两个项目的间距都相同。

align-items

align-items属性定义项目在交叉轴上如何对齐。

1
2
3
.box {
align-items: flex-start | flex-end | center | baseline | stretch;
}

它可能取5个值。具体的对齐方式与交叉轴的方向有关,下面假设交叉轴从上到下。

  • flex-start:交叉轴的起点对齐。
  • flex-end:交叉轴的终点对齐。
  • center:交叉轴的中点对齐。
  • baseline:项目的第一行文字的集中县对齐。
  • stretch:默认值。如果项目未设置宽高或设为auto,将占满整个容器的高度。

align-content

align-content属性定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用。

1
2
3
.box {
align-content: flex-start | flex-end | center | space-between | space-around | stretch;
}
  • flex-start:与交叉轴的起点位置对齐。
  • flex-end:与交叉轴的终点位置对齐。
  • center:与交叉轴的中点位置对齐。
  • space-between:与交叉轴两端对齐,轴线之间间距平均分布。
  • spance-around:你每根轴线两侧的间距都相等。所以,轴线之间的间隔比轴线与边框的间隔大一倍。
  • stretch:默认值。轴线站面整个交叉轴。

父容器属性

  • order
  • flex-grow
  • flex-shrink
  • flex-basis
  • flex
  • align-self

order

order 属性定义项目的排列顺序。数值越小,排列越靠前,默认为0.

1
2
3
.item {
order: <integer>;
}

flex-grow

flex-grow属性定义项目的放大比例,默认为0,即如果存在剩余空间,也不放大。

1
2
3
.item {
flex-grow: <number>; /* default 0 */
}

如果所有项目的flex-grow属性都为1,则它们将等分剩余空间(如果有的话)。如果一个项目的flex-grow属性为2,其他项目都为1,则前者占据的剩余空间将比其他项多一倍。

flex-shrink

flex-shrink属性定义了项目的缩小比例,默认为1,即如果空间不足,该项目将缩小。

如果所有项目的flex-shrink属性都为1,当空间不足时,都将等比例缩小。如果一个项目的flex-shrink属性为0,其他项目都为1,则空间不足时,前者不缩小。

负值对该属性无效。

flex-basis

flex-basis属性定义了在分配多余空间之前,项目占据的主轴空间(main size)。浏览器根据这个属性,计算主轴是否有多余空间。它的默认值为auto,即项目的本来大小。

1
2
3
.item {
flex-basis: <length> | auto; /* default auto */
}

它可以设为跟widthheight属性一样的值(比如350px),则项目将占据固定空间。

flex

flex属性是flex-grow, flex-shrinkflex-basis的简写,默认值为0 1 auto。后两个属性可选。

1
2
3
.item {
flex: none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ]
}

该属性有两个快捷值:auto (1 1 auto) 和 none (0 0 auto)。

建议优先使用这个属性,而不是单独写三个分离的属性,因为浏览器会推算相关值。

align-self

align-self属性允许单个项目有与其他项目不一样的对齐方式,可覆盖align-items属性。默认值为auto,表示继承父元素的align-items属性,如果没有父元素,则等同于`stretch。

1
2
3
.item {
align-self: auto | flex-start | flex-end | center | baseline | stretch;
}

该属性可能取6个值,除了auto,其他都与align-items属性完全一致。

前端进阶系列—css3新特性

什么是CSS?

层叠样式表(CSS)是一种向Web文档添加样式(例如,字体,颜色,间距)的简单机制。

什么是CSS3?

CSS3是CSS语言的最新发展,旨在扩展CSS2.1。它带来了许多新功能和附加功能,如圆角,阴影,渐变,过渡或动画,以及多列,灵活的框或网格布局等新布局。

现在让我们看看有哪些新东西?

CSS3选择器

选择器是CSS的核心。最初,CSS允许按类型,类和/或ID匹配元素。CSS2.1添加了伪元素,伪类和组合子。使用CSS3,我们可以使用各种选择器来定位页面上的几乎任何元素。

CSS2引入了几个属性选择器。这些允许基于其属性匹配元素。 CSS3扩展了那些属性选择器。在CSS3中添加了三个属性选择器;它们允许子串选择。

  • 匹配属性attr以值val开头的任何元素E.换句话说,val匹配属性值的开头。
1
2
E[attr^=val]
eg. a[href^='http://sales.']{color: teal;}
  • 匹配属性attr以val结尾的任何元素E.换句话说,val匹配属性值的结尾。
1
2
E[attr$=val]
eg. a[href$='.jsp']{color: purple;}
  • 匹配属性attr在属性中的任何位置匹配val的任何元素E.它类似于E [attr~ = val],在这里val可以是单词也可以是单词的一部分。
1
2
3
4
E[attr*=val]  
eg. img[src*='artwork']{
border-color: #C3B087 #FFF #FFF #C3B087;
}

伪类元素

您可能已经熟悉了一些用户交互伪类,即:link,:visited,:hover,:active和:focus。

在CSS3中添加了一些伪类选择器。一个是:根选择器,它允许设计者指向文档的根元素。在HTML中,它将是。因为:root是通用的,它允许设计者选择XML文档的根元素,而不必知道它的名称。要在文档中需要时允许滚动条,此规则将起作用。

1
:root{overflow:auto;}

作为:first-child选择器的补充,添加了:last-child。有了它,可以选择父元素命名的最后一个元素。

1
div.article > p:last-child{font-style: italic;}

添加了一个新的用户交互伪类选择器:target目标选择器。为了在用户点击同一页面链接时将用户的注意力吸引到一段文本,像下面第一行这样的规则可以很好地工作;链接看起来像第二行,突出显示的跨度就像第三行。

1
2
3
span.notice:target{font-size: 2em; font-style: bold;}
<a href='#section2'>Section 2</a>
<p id='section2'>...</p>

已创建用于选择未通过测试的指定元素的功能表示法。否定伪类选择器:不能与几乎任何已实现的其他选择器耦合。例如,要在没有指定边框的图像周围放置边框,请使用这样的规则。

1
img:not([border]){border: 1;}

CSS3颜色

CSS3带来了对一些描述颜色的新方法的支持。在CSS3之前,我们几乎总是使用十六进制格式(#FFF或#FFFFFF for white)声明颜色。也可以使用rgb()表示法声明颜色,提供整数(0-255)或百分比。

颜色关键字列表已在CSS3颜色模块中进行了扩展,包括147种额外的关键字颜色(通常得到很好的支持),CSS3还为我们提供了许多其他选项:HSL,HSLA和RGBA。这些新颜色类型最显着的变化是能够声明半透明颜色。

RGBA

RGBA的工作方式与RGB类似,只是它添加了第四个值:alpha,不透明度级别或alpha透明度级别。前三个值仍然代表红色,绿色和蓝色。对于alpha值,1表示完全不透明,0表示完全透明,0.5表示50%不透明。您可以使用介于0和1之间的任意数字。

1
background: rgba(0,0,0,.5) //在这里0.5的0可以省略

HSL和HSLA

HSL代表色调,饱和度和亮度。与RGB不同,您需要通过一致地更改所有三个颜色值来操纵颜色的饱和度或亮度,使用HSL,您可以调整饱和度或亮度,同时保持相同的基色调。 HSL的语法包括色调的整数值,以及饱和度和亮度的百分比值。

hsl()声明接受三个值:

  • 0到359度的色调。一些例子是:0 =红色,60 =黄色,120 =绿色,180 =青色,240 =蓝色,300 =品红色。
  • 饱和度为百分比,100%为常态。 100%的饱和度将是完整的色调,饱和度0将给你一个灰色阴影。
  • 基本上导致色调值被忽略。
  • 饱和度为百分比,100%为常态。 100%的饱和度将是完整的色调,饱和度0将给你一个灰色阴影》
  • 基本上导致色调值被忽略。
  • 轻度的百分比,50%是常态。亮度为100%为白色,50%为实际色调,0%为黑色。

    hsla()中的a也与rgba()中的函数相同

    Opacity

    除了使用HSLA和RGBA颜色指定透明度(以及很快,八位十六进制值)之外,CSS3还为我们提供了不透明度属性。 opacity设置声明它的元素的不透明度,类似于alpha。

    我们来看一个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
     div.halfopaque {
    background-color: rgb(0, 0, 0);
    opacity: 0.5;
    color: #000000;
    }
    div.halfalpha {
    background-color: rgba(0, 0, 0, 0.5);
    color: #000000;
    }

尽管alpha和不透明度符号的使用看似相似,但是当你看它时,它们的功能有一个关键的区别。

虽然不透明度为元素及其所有子元素设置不透明度值,但半透明RGBA或HSLA颜色对元素的其他CSS属性或后代没有影响。

圆角:边界半径

border-radius属性允许您创建圆角而无需图像或其他标记。要在我们的框中添加圆角,我们只需添加即可

1
border-radius: 25px;

border-radius属性实际上是一种速记。对于我们的“a”元素,角落大小相同且对称。如果我们想要不同大小的角落,我们可以声明最多四个唯一值

1
border-radius: 5px 10px 15px 20px;

阴影

CSS3提供了使用box-shadow属性向元素添加阴影的功能。此属性允许您指定元素上一个或多个内部和/或外部阴影的颜色,高度,宽度,模糊和偏移。

1
box-shadow: 2px 5px 0 0 rgba(72,72,72,1);

文字阴影

text-shadow为文本节点中的单个字符添加阴影。在CSS 3之前,可以通过使用图像或复制文本元素然后定位它来完成。

1
text-shadow: topOffset leftOffset blurRadius color;

线性渐变

W3C添加了使用CSS3生成线性渐变的语法。

1
2
3
4
Syntax: background: linear-gradient(direction, color-stop1, color-stop2, ...);
e.g. #grad {
background: linear-gradient(to right, red , yellow);
}


你甚至可以用度数指定方向,例如在上面的例子中,60deg而不是右边。

径向渐变

径向渐变是圆形或椭圆形渐变。颜色不是沿着直线前进,而是从所有方向的起点混合出来。

1
2
3
4
5
6
7
Syntax : background: radial-gradient(shape size at position, start-color, ..., last-color);
e.g. #grad {
background: radial-gradient(red, yellow, green);
}//Default
#grad {
background: radial-gradient(circle, red, yellow, green);
}//Circle

背景

在CSS3中,不需要为每个背景图像包含一个元素;它使我们能够向任何元素添加多个背景图像,甚至伪元素。

1
2
3
4
background-image:
url(firstImage.jpg),
url(secondImage.gif),
url(thirdImage.png);

这些是新实现的CSS3功能,还有其他未实现的功能,我们将在实施后讨论它们。

望大家有所收获,下面是我的公众号,定期更新学习资料:

前端工程化之路

时至今日,前端也在飞速发展,各种框架以及因框架产生的生态圈也拔地而起,满天都是概念,如数据绑定、虚拟DOM、前端构建系统、前端工程化、大前端等等。很多前端同学被困在前端框架里,学的是心力交瘁,但有些同学早已学会了透过现象看本质的本事,不管是什么样的框架都能运用自如。
下面就带大家先看看前端的发展历史。

前端发展史

  • 1990年,Tim Berners Lee 发明了世界上第一个网页浏览器。

浏览器是被发明出来了,但问题来了,当时的网页都是静态的,如表单验证就需要后端完成,在那个年代带宽很差,发一次请求带代价是极其昂贵的,因此为了解决这一问题,急需要开发一种在浏览器执行的脚本语言,因此发生了下面的事情。

  • 1995年,Brendan Eich 只用了不到半个月时间完成了第一版网页脚本语言的设计。

  • 1996年,样式表标准CSS第一版发布。

至此 htmljavascriptcss 催生了前端的雏形。

  • 2005年,Ajax方法正式诞生。
  • 2006年,jQuery函数库诞生,为操作网页DOM结构提供了非常强大一用的接口,成为了使用最广泛的函数库,并让JavaScript语音的应用难度大大降低,推动了这门语言的流行。
  • 2008年,V8编译器诞生。
  • 2009年,Node.js项目诞生,标志着JavaScript可以用于服务器端变成。
  • 同年,Angularjs项目诞生。
  • 2013年,Facebook发布了UI框架React,使得UI层可以组件化开发。
  • 2014年2月,尤雨溪发布了个人项目Vue,这是一个通过间接API提供高效的数据绑定和灵活的组建系统。
  • 2015年3月,Facebook发布了React Native,将React框架移植到了手机端,可以用来开发手机App,它会将JavaScript转为ios平台的Objective-C代码,或者Android平台的Java代码,从而为JavaScript语言开发高性能的原生App打开了一条道路。

至此新事物仍在不断涌现。

前后端协作发展史

简单明快的石器时代

这是个混沌的时代,项目开发没有前后端之分,页面是有JSP,PHP在服务端生成,前端只是个切图仔。当时对前端开发并不友好,前端开发需要安装后端开发环境,拉取后端项目,即便是改个样式也需要运行后端环境来验证改的是否正确,难以进行本地化,影响其研发效率。

image

以后端MVC为主的青铜时代

为了让前后端分工更加合理,提高代码维护性,协作开发也在不断演化。因此有了后端的架构升级。比如Structs、Spring MVC等。

代码可维护性得到了明显好转,为了让View层更简单干脆,还可以选择Velocity、Freemaker等模板,使得模板里写不了Java代码,让前后端分工更加清晰。那么问题又来了,前端还是需要依赖后端开发环境。这种架构下,前端写demo,后端套模板。

前后端职责依旧纠缠不清,在套模板的同时也会出一些粗心引起的问题,前端的路依旧很艰辛,继续往下看。

Ajax带来了白银时代

回归第一章讲的, 2005年,Ajax方法正式诞生。前后端之间的协作有了质的变化。

图片

其关键的协作点就是Ajax提供数据接口,就此复杂度从JSP里已到了浏览器的JavaScript,浏览器端变得很复杂,类似于Spring MVC,这个时代的浏览器端也出现了分层架构:

图片

但仍不足之处:

  1. 代码不能复用。比如后端依旧需要对数据做各种校验,校验逻辑无法复用浏览器端的代码。如果可以复用,那么后端的数据校验可以相对简单化。
  2. 全异步,对 SEO 不利。往往还需要服务端做同步渲染的降级方案。
  3. 性能并非最佳,特别是移动互联网环境下。
  4. SPA 不能满足所有需求,依旧存在大量多页面应用。URL Design 需要后端配合,前端无法完全掌控。

Node带来了黄金时代

随着 Node.js 的兴起,JavaScript 开始有能力运行在服务端。这意味着可以有一种新的研发模式:

虽然通过Node.js也可以像Java一样提供后端支持,通过Node.js,服务端代码也是JavaScript代码。这意味着部分代码可以复用,需要SEO的场景可以在服务端同步渲染,由于异步请求太多导致的性能问题也可以通过服务端来缓解。前一种模式的不足,通过这种模式几乎都能完美解决掉。

与JSP模式相比,该模式看起来是一种回归,也的确是一种向原始开发模式的回归,不过是一种螺旋上升式的回归。

除了对服务端开发的贡献以外还丰富了前端的工具生态。在没有Grunt、Gulp、webpack等便捷且强大的工具的时候,静态资源的压缩合并,都是需要像Apache Ant这用工具完成的,但Node的出现,带来了Grunt、Gulp、webpack,结合JavaScript的灵活性与Node.js提供的API,前端工程师可以编写各种工具满足项目的开发需求。 本书所介绍的前端工程化解决方案中的各个功能模块全部是由JavaScript语言实现的。

前端工程化钻石时代

经过了以上几个时代,诞生了当下最流行的几个框架:React、Vue、Angular,前端工程化也在这里发挥的淋漓尽致,前几个时代积累了大量的经验和工具,这个时代则将在前辈们的基础之上解决这个历史时代遗留的痛点,比如:

  • 扩展javascript 、html、css本身的语言能力
  • 解决重复工作的问题
  • 模板化、模块化
  • 解决功能复用和变更问题
  • 解决开发和产品差异的问题
  • 解决发布流程的问题

何为工程化

所有能降低开发成本,并且能提高开发效率的事情总称为工程化。

在实际的工作和产品研发中,我不觉得还有什么事情比降低成本,提高效率更迫切的事情。我更不认同工程化只是项目经理,技术 Leader 去研究和推广的事情。每个团队都是不一样,技术栈不一样,产品不一样,工作环境背景不一样。大公司有大团队,多部门合作。小公司有小团队,各种职能配合更密切,或者你身兼数职,但是并不妨碍工程化的推进,你作为团队的一员,非常有义务和必要一起推进工程化,找到符合你们团队的工作习惯和规范。

因为,工程化的推进只是为了提高效率和降低成本。这里说的效率和成本,并不只是公司层面,还包括个人。良好的工程化,能降低沟通成本,实现更好的协同,节省开发和测试人员的重复劳动,降低发布的常见问题等等,经过有效实践,工程化的推广还能极大地减少加班的时间。

后面会有一系列文章讲讲具体怎样实现工程化。


参考链接:https://github.com/lifesinger/blog/issues/184
参考链接:https://segmentfault.com/a/1190000007414499