0%

命令式框架与声明式框架:

命令式框架:关注过程(Jquery)

1
2
3
$('#app')
.text('hello world')
.on('click',()=>alert('ok'))

声明式框架:关注结果(Vue.js)

1
<div @click='()=>alert('ok')'></div>

结论:声明式代码的性能不优于命令式代码的性能

声明式代码的更新性能消耗=找出差异的性能消耗+直接修改的性能消耗

Vue3中虚拟DOM的性能:

创建JavaScript对象的计算量+创建真实DOM的计算量

比较:

纯JavaScript:Js运算+DOM运算(性能因素)

虚拟DOM:创建Js对象(Vnode)+Diff+必要的DOM更新(与数据量变化有关)

innerHTML:渲染HTML字符串+销毁所有旧DOM,新建所有新DOM(与模板大小有关)

结论:

性能

innerHTML<虚拟DOM<原始JavaScript

心智负担

虚拟DOM<innerHTML<原生JavaScript

可维护性

虚拟DOM>innerHTML和原生JS

框架的设计可以有三种选择:

  • 纯运行时
  • 运行时+编译时(Vue3)
  • 纯编译时

1.纯运行时

一个树型结构的数据对象

1
2
3
4
5
6
const obj={
tag:'div',
children:[
{tag:'span',children:'hello world'}
]
}

tag:标签名,children:子节点

render:

1
2
3
4
5
6
7
8
9
10
function Render(obj,root){
const el = document.createElement(obj.tag)
if(typeof children ==='string'){
const text=document.createTextNode(obj,children)
el.appendChild(text)
}else if(obj.children){
obj.children.forEach((child)=>Render(child,el))
}
root.appendChild(el)
}
1
Render(obj,document.body)

2.运行时+编译时

编写一个Compiler函数,把HTML标签编译成树型结构的对象

1
2
3
4
5
const html=`<div>
<span>Hello world</span>
</div>`
const obj=Compiler(html)
Render(obj,document.body)

3.纯编译式:(Svelte)

1
2
3
4
5
6
7
8
<div><span>hello world</span></div>
//就是
const div=document.createElement('div')
const span=document.createElement('span')
const text=document.createElement('hello wrold')
span.appendChild(text)
div.appendChild(span)
document.body.appendChild(div)

image-20220727225427188

生命周期执行顺序:

创建时:

  • constructor()

  • static getDerivedStateFromProps()

  • render()

    是class组件中唯一必须实现的方法,用于渲染dom,render()方法必须返回reactDOM

    不要在render里面setState,否则会触发死循环导致内存崩溃

  • componentDidMount()

    在组件挂载后立即调用,componentDidMount()是发送网络请求,启用事件监听方法的好时机,并且可以在此钩子函数里直接调用setState()

更新时:

  • static getDerivedStateFromProps()

  • shouldComponentUpdate()

    在组件更新之前调用,可以控制组件是否进行更新,返回true组件更新,返回false则不更新

    shouldComponentUpdate(nextProps,nextState),第一个是即将更新的props值,第二个是即将更新后的state值,可以根据更新前后的props或state来比较加一些限制条件,决定是否更新,进行性能优化,不要 shouldComponentUpdate 中调用 setState(),否则会导致无限循环调用更新、渲染,直至浏览器内存崩溃

  • render()

  • getSnapshotBeforeUpdate()

  • componentDidUpdate()

    会在更新后被立即调用,首次渲染不会执行。包含三个参数,第一个是上一次props值,第二个是上一次state值,如果组件实现了getSnapshotBeforeUpdate()生命周期,第三个snapshot参数传递

卸载时:

  • componentWillUnmount()

在组件即将被卸载或销毁时进行调用,是取消网络请求,移除监听事件,清理DOM元素,清理定时器等操作的好时机

新生命周期钩子:

1.

1
static getDerivedStateFromProps(props,state)

getDerivedStateFromProps 会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。它应返回一个对象来更新 state,如果返回 null 则不更新任何内容。

此方法适用于罕见的用例,即 state 的值在任何时候都取决于 props。

2.

1
getSnapshotBeforeUpdate(prevProps,prevState)

getSnapshotBeforeUpdate()在最近一次渲染输出(提交到DOM结点)之前调用,它使得组件能在发生更改之前从DOM中捕获一些信息,此生命周期方法的任何值返回值将作为参数传递给componentDidUpdate()

例子:

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
class ScrollingList extends React.Component {
constructor(props){
super(props);
this.listRef=React.createRef();

}
getSnapshotBeforeUpdate(prevProps,prevState){
if(prevProps.list.length<this.props.list.length){
const list = this.listRef.current;
return list.scrollHeight-list.scrollTop
}
return null;
}
componentDidUpdate(prevProps,prevState,snapshot){
if(snapshot!=null){
const list=this.listRef.current;
list.scrollTop=list.scrollHeight-snapshot;
}
}
render(){
return(
<div ref={this.listRef}></div>
)
}
}

理解:

一种组件间通信方式,用于祖组件和后代组件之间的通信

1.使用Context

2.创建Context容器对象

1
const xxxContext = React.createContext(defaultValue)

3.渲染子组件,外面包裹xxxContext.Provider,通过value属性给后代组件传递数据

1
2
<xxxContext.Provider value={数据}>
</xxxContext.Provider>

4.后代组件读取数据
第一种适用于类组件:

1
2
static contextType = xxxContext;//声明接收context
this.context//context是一个对象,读取context中value的值

第二种函数组件和类组件都可以:

1
2
3
4
5
6
7
8
<xxxContext.Consumer>
{
value=>(//value是context中的value数据
要显示的内容
)

}
</xxxContext.Consumer>

例子:

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
33
const ThemeContext = React.createContext({
value:"blue"
})
class A extends React.Component {
constructor(props){
super(props)
this.state={value:"red"}
}
render(){
return(
<ThemeContext.Provider value={this.state}>
<B/>
</ThemeContext.Provider>
)
}
}
function B(){
return(
<div>
<C/>
</div>
)
}
function C(){
return (
<ThemeContext.Consumer>
{({value})=>(
<h1>{value}</h1>
)}
</ThemeContext.Consumer>

)
}

context一般用于封装react插件

使用useContext

1
const value = useContext(MyContext)

接收一个context对象(React.createContext的返回值)并返回该context的当前值,当前的context值由上层㢟中距离当前组件最近的<MyContext.Provider>的value prop角色

当组件上层最近的<MyContext.Provider>更新时,该Hook会触发重渲染,使用最新传递给MyContext provider的context value值。

useContext(MyContext)相当于static contextType=MyContext或者<MyContext.Consumer>

useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context。

概念:

Redux 是一个使用”actions”的事件管理和更新应用状态的模式和工具库,以集中式Store的方式对整个应用中使用的状态进行集中管理,其规则确保状态只能以可预测的方式更新

Redux 在以下情况下更有用:

  • 在应用的大量地方,都存在大量的状态
  • 应用状态会随着时间的推移而频繁更新
  • 更新该状态的逻辑可能很复杂
  • 中型和大型代码量的应用,很多人协同开发

store:

保存应用程序的全局state的容器,是一个Js对象,store是通过传入一个reducer来创建的,并且有一个名为getState的方法,它返回当前状态值

1
2
3
4
import {configureStore} from "@reduxjs/toolkit"
const store=configureStore({reducer:counterReducer})
console.log(store.getState())
//{value:0}

action:

是一个具有type字段的普通js对象,可以将action视为描述应用程序中发生了什么事件,type字段是一个字符串,给这个action一个描述性的名字,比如”todos/todoAdded”(域/事件名称),第一部分是这个action所属的特征和类别,第二部分是具体发生的具体事情

action对象可以有其他字段,将其放在名为payload的字段中

action创建函数:就是生成action的方法,action创建函数只是简单返回一个action

1
2
3
4
5
6
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}

reducers

reducer是一个函数,接收当前的state和一个action对象,必要时决定如何更新状态,并返回新状态,函数签名(state,action)=>newState,可以将reducer视为一个事件监听器,它根据接收到的action类型处理事件

reducer必须是一个纯函数:

  • 仅使用 stateaction 参数计算新的状态值
  • 禁止直接修改 state。必须通过复制现有的 state 并对复制的值进行更改的方式来做 不可变更新(immutable updates)
  • 禁止任何异步逻辑、依赖随机值或导致其他“副作用”的代码

reducer 函数内部的逻辑通常遵循以下步骤:

  • 检查 reducer 是否关心这个 action
    • 如果是,则复制 state,使用新值更新 state 副本,然后返回新 state
  • 否则,返回原来的 state 不变

dispatch

更新state的唯一方法是调用store.dispatch()并传入一个action对象,store将执行所有reducer函数并计算出更新后的state,调用getState()可以获取更新的state

1
2
3
store.dispatch({type:'counter/increamented'})
console.log(store.getStore())
//{value:1}

Selectors:

Selector函数可以从store状态树中提取指定的片段,随着应用变大,遇到不同应用程序的不同部分需要读取相同数据,selector可以避免重复这样的状态逻辑

1
2
3
4
const selectCounterValue=state=>state.value
const currentValue = selectCounterValue(store.getState())
console.log(currentValue)
//2

核心概念:

单一数据源:应用程序的全局状态作为对象存储在单个 store 中。任何给定的数据片段都应仅存在于一个位置,而不是在许多位置重复。

state只读

更改状态的唯一方法是 dispatch 一个 action,这是一个描述所发生情况的对象。

这样,UI 就不会意外覆盖数据,并且更容易跟踪发生状态更新的原因。由于 actions 是普通的 JS 对象,因此可以记录、序列化、存储这些操作,并在以后重放这些操作以进行调试或测试。

使用reducer纯函数进行更改:

Reducers 是纯函数,它们采用旧 state 和 action,并返回新 state。

redux数据流:

单向数据流:

root reducer函数创建Redux store=>store调用一次root reducer,并将返回值保存为它的初始state=>UI首次渲染时,UI组件访问Redux store的当前state,并将数据渲染为内容,监听store的更新:

应用更新=>dispatch一个action到Redux store=>store用之前的state和当前的action再次运行reducer函数,并将返回值保存为新的state=>store通知所有订阅过的UI,通知store更新=>每个订阅过store数据的UI组件就会检查它们需要的state部分是否被更新=>发现更新,每个组件强制使用新数据渲染,更新网页

明确两个概念:

UI组件:不能使用任何redux的api,只负责页面的呈现,交互

容器组件:负责和redux通信,将结果交给UI组件

  • 创建一个容器组件:靠react-redux的connect函数,connect(mapStateToProps,mapDispatchToProps)(UI组件)
  • mapStateToProps:映射状态:返回值时一个对象
  • mapDispatchToProps:映射操作状态的方法,返回值是一个对象
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
33
34
35
36
import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'

const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
case 'SHOW_ALL':
default:
return todos
}
}

const mapStateToProps = state => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}

const mapDispatchToProps = dispatch => {
return {
onTodoClick: id => {
dispatch(toggleTodo(id))
}
}
}

const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)

export default VisibleTodoList
  • 容器组件中的store是靠props传进去,而不是在容器组件中直接引入

优化:

  • 容器组件和UI组件整合为一个文件
  • 不用给容器组件传递store,给包裹一个即可
  • 使用react-redux后不用自己检测redux状态的变化,容器组件自己完成这个工作
  • mapDispatchToProps写成一个对象
1
2
3
{
onTodoClick:toggleTodo
}

总结:一个组件与redux打交道步骤:

  • 定义UI组件不暴露
  • 引入connect生成一个容器组件:connect(state=>{key:value}),{key:xxxAction})(UI组件)
  • 在UI组件中通过this.props.xxx读取和操作状态

todoList例子:

https://www.redux.org.cn/docs/basics/ExampleTodoList.html

Partial

构造一个所有属性的Type都设置为可选的类型,返回一个表示给定类型的所有子集的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Todo {
title:string;
description:stirng;
}
function updateTodo(todo:Todo,fieldsToUpdate:Partial<Todo>){
return {...todo,...fieldsToUpdate}
}
const todo1 = {
title: "organize desk",
description: "clear clutter",
};

const todo2 = updateTodo(todo1, {
description: "throw out trash",
});

Required

构造一个由所有属性类型都是required的类型,与Partial相反

1
2
3
4
5
6
7
8
9
interface Props {
a?: number;
b?: string;
}

const obj: Props = { a: 5 };

const obj2: Required<Props> = { a: 5 };
//Property 'b' is missing in type '{ a: number; }' but required in type 'Required<Props>'.

Readonly

构造一个所有属性的Type都设置为readonly的类型,这意味着构造类型的属性不能重新分配

1
2
3
4
5
6
7
8
9
10
interface Todo {
title: string;
}

const todo: Readonly<Todo> = {
title: "Delete inactive users",
};

todo.title = "Hello";
//Cannot assign to 'title' because it is a read-only property.

Record<Keys,Type>

构造一个对象类型,其属性键为keys,属性值为Type,可用于将一种类型的属性映射到另一种类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface CatInfo {
age: number;
breed: string;
}

type CatName = "miffy" | "boris" | "mordred";

const cats: Record<CatName, CatInfo> = {
miffy: { age: 10, breed: "Persian" },
boris: { age: 5, breed: "Maine Coon" },
mordred: { age: 16, breed: "British Shorthair" },
};

cats.boris;

//const cats: Record<CatName, CatInfo

Pick<Type,Keys>

通过从中选择一组属性keys来构造类型Type

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Todo {
title: string;
description: string;
completed: boolean;
}

type TodoPreview = Pick<Todo, "title" | "completed">;

const todo: TodoPreview = {
title: "Clean room",
completed: false,
};

todo;

//const todo: TodoPreview

omit<Type,Keys>

Type通过从中选择所有属性然后删除keys来构造类型

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
interface Todo {
title: string;
description: string;
completed: boolean;
createdAt: number;
}

type TodoPreview = Omit<Todo, "description">;

const todo: TodoPreview = {
title: "Clean room",
completed: false,
createdAt: 1615544252770,
};

todo;

//const todo: TodoPreview

type TodoInfo = Omit<Todo, "completed" | "createdAt">;

const todoInfo: TodoInfo = {
title: "Pick up kids",
description: "Kindergarten closes at 5pm",
};

todoInfo;

//const todoInfo: TodoInfo

Exclude<UnionType,ExcludedMembers>

通过从UnionType中排除可以赋值给ExcludedMembers的值

1
2
3
4
5
6
7
type T0=Exclude<"a"|"b"|"c","a">
//type T0="b"|"c"
type T1=Exclude<"a"|"b"|"c","a"|"b">;
//type T1="c"
type T2 = Exclude<string | number | (() => void), Function>;

//type T2 = string | number

Extract<Type,Union>

提取出Type和Union的交集(可以赋值给Union成员的值)

1
2
3
4
5
6
type T0 = Extract<"a" | "b" | "c", "a" | "f">;

//type T0 = "a"
type T1 = Extract<string | number | (() => void), Function>;

//type T1 = () => void

NonNullable

构造一个类型,可以从Type中排除null和undefined

1
2
3
4
5
6
type T0 = NonNullable<string | number | undefined>;

//type T0 = string | number
type T1 = NonNullable<string[] | null | undefined>;

//type T1 = string[]

ReturnType

由函数返回类型组成(Type必须是一个(…args:any)=>any)

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
33
34
35
declare function f1(): { a: number; b: string };

type T0 = ReturnType<() => string>;

//type T0 = string
type T1 = ReturnType<(s: string) => void>;

//type T1 = void
type T2 = ReturnType<<T>() => T>;

//type T2 = unknown
type T3 = ReturnType<<T extends U, U extends number[]>() => T>;

//type T3 = number[]
type T4 = ReturnType<typeof f1>;

//type T4 = {
// a: number;
// b: string;
//}
type T5 = ReturnType<any>;

//type T5 = any
type T6 = ReturnType<never>;

//type T6 = never
type T7 = ReturnType<string>;
//Type 'string' does not satisfy the constraint '(...args: any) => any'.

//type T7 = any
type T8 = ReturnType<Function>;
//Type 'Function' does not satisfy the constraint '(...args: any) => any'.
// Type 'Function' provides no match for the signature '(...args: any): any'.

//type T8 = any

官网链接:https://www.typescriptlang.org/docs/handbook/utility-types.html#returntypetype

什么是函数式编程?

简单来说,函数式编程是一种强调以函数使用为主的软件开发风格。

还有一点你要记住,函数式编程的目的是使用函数来抽象作用在数据之上的控制流和操作,从而在系统中消除副作用减少对状态的改变。

声明式编程

函数式编程属于声明式编程范式:这种范式会描述一系列的操作,但并不会暴露它们是如何实现的或是数据流如何传过它们

我们所熟知的 SQL 语句就是一种很典型的声明式编程,它由一个个描述查询结果应该是什么样的断言组成,对数据检索的内部机制进行了抽象

我们再来看一组代码再来对比一下命令式编程和声明式编程。

1
2
3
4
5
6
7
8
9
10
11
// 命令式方式
var array = [0, 1, 2, 3]
for(let i = 0; i < array.length; i++) {
array[i] = Math.pow(array[i], 2)
}

array; // [0, 1, 4, 9]

// 声明式方式
[0, 1, 2, 3].map(num => Math.pow(num, 2))

可以看到命令式很具体的告诉计算机如何执行某个任务。

而声明式是将程序的描述与求值分离开来。它关注如何用各种表达式来描述程序逻辑,而不一定要指明其控制流或状态关系的变化。

纯函数

纯函数是指没有副作用的函数,相同的输入有相同的输出。

1
2
3
4
var counter=0
function increment(){
return ++counter;
}

这个函数是不纯的,它读取了外部变量。这种依赖外部变量进行的计算,结果很难预测,可能在某些地方修改了counter的值,导致increment出来的值不是预期的

对于纯函数有性质:

  • 仅取决于提供的输入,而不依赖于任何在函数求值或调用间隔时可能变化的隐藏状态和外部状态
  • 不会造成超出作用域的变化,例如修改全局变量或引用传递的参数

对于相同的输入,纯函数的输出结果总是一致的,可预测的,这个也称为引用透明

引用透明

引用透明是定义一个纯函数较为正确的方法,纯度在这个意义上表示一个函数的参数和返回值之间的纯度关系,如果一个函数对于相同的输入始终产生相同的结果,我们就说它是引用透明

1
2
3
4
5
6
7
// 非引用透明
var counter = 0
function increment() {
return ++counter
}
// 引用透明
var increment = (counter) => counter + 1

不可变数据

不可变数据指的是创建后不能更改的数据,与其他语言一样,js里有一些基本类型(String,NUmber等)从本质上是不可变的,但是对象就是在任意的地方可变

例如:

1
2
3
4
5
6
7
8
9
var sortDesc = function(arr) {
return arr.sort(function(a, b) {
return a - b
})
}

var arr = [1, 3, 2]
sortDesc(arr) // [1, 2, 3]
arr // [1, 2, 3]

这段代码会导致在排序中产生副作用,原始数组arr被修改了

总结

  • 使用纯函数的代码绝不会更改或破坏全局状态,有助于提高代码的可测试性和可维护性
  • 函数式编程采用声明式的风格,易于推理,提高代码的可读性。
  • 函数式编程将函数视为积木,通过一等高阶函数来提高代码的模块化和可重用性。

内容来至于《JavaScript函数式编程指南》

虚拟结点+diff算法部分

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106

function h(tag,props,children){
return {
tag,
props,
children
}
}
function mount(vnode,container){
//创建相应的真实DOM结点
const el=vnode.el=document.createElement(vnode.tag);
//props
if(vnode.props){
for(const key in vnode.props){
const value=vnode.props[key];
el.setAttribute(key,value);

}
}
//children

if(vnode.children){
if(typeof vnode.children=='string'){
el.textContent=vnode.children;

}
else{
vnode.children.forEach(child=>{
mount(child,el)//递归
})
}
}
container.appendChild(el);


}
const vdom=h('div',{class:'red'},[
h('span',null,['hello'])
])
const vdom2=h('div',{class:'red'},[
h('span',null,['hi'])
])
mount(vdom,document.getElementById('app'))
function patch(n1,n2){
const el=n2.el=n1.el;
if(n1.tag===n2.tag){

//props
const oldProps=n1.props||{};
const newProps=n1.props||{};
for(const key in newProps){
const oldValue=oldProps[key];
const newValue=newProps[key]
if(newValue!==oldValue){
el.setAttribute(key,newValue)
}
}
for(const key in oldProps){
if(!key in newProps){
el.removeAttribute(key)
}
}
//children
const oldChildren=n1.children;
const newChildren=n2.children;
if(typeof newChildren==='string'){
if(typeof oldChildren==='string'){
if(newChildren!==oldChildren){
el.textContent=newChildren;
}
}else{
el.textContent=newChildren
}
}else {
//if newChildren is array
if(typeof oldChildren==='string'){
el.innerHTML=''
newChildren.forEach(child=>{
mount(child,el)
})
}else{
const commonLength=Math.min(oldChildren.length,newChildren.length)
for(let i=0;i<commonLength;i++){
patch(oldChildren[i],newChildren[i])
}
if(newChildren.length>oldChildren.length){
newChildren.slice(oldChildren.length).forEach(child=>{
mount(child,el)
})

}else if(newChildren.length<oldChildren.length){
oldChildren.slice(newChildren.length).forEach(child=>{
el.removeChild(child.el);
})
}
}
}

}else{
//replace

}

}
patch(vdom,vdom2);

reactive部分

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86

let activeEffect;
class Dep{
subscribers=new Set()
depend(){
if(activeEffect){
this.subscribers.add(activeEffect)
}
}
notify(){
this.subscribers.forEach(effect=>{
effect()
})
}

}
//Vue2
/*function reactive(raw){
Object.keys(raw).forEach(key=>{
const dep=new Dep()
let value=raw[key]
Object.defineProperty(raw,key,{
get(){
dep.depend()
return value

},
set(newValue){
value=newValue
dep.notify()

}
})
})
return raw

}*/
const targetMap=new WeakMap()//键值可以是对象,而且会被自动垃圾回收
function getDep(target,key){
let depsMap=targetMap.get(target)
if(!depsMap){
depsMap=new Map()
targetMap.set(target,depsMap)
}
let dep=depsMap.get(key)
if(!dep){
dep=new Dep()
depsMap.set(key,dep)
}

return dep;

}
const reactiveHandler={
get(target,key,receiver){
let dep=getDep(target,key)
dep.depend()
return Reflect.get(target,key,receiver)//Reflect不会抛出异常,只会抛出真假
},
set(target,key,value,receiver){
let dep=getDep(target,key)
const result=Reflect.set(target,key,value,receiver)
dep.notify()
return result

}

}
function reactive(raw){
return new Proxy(raw,reactiveHandler)//Proxy 会触发set get 有利于数组观测,不必多写一些数组内置方法
}

function watchEffect(effect){
activeEffect=effect
effect()
activeEffect=null
}
const dep=new Dep()
const state=reactive({
count:0
})
watchEffect(()=>{
console.log(state.count)
})//effect run

state.count++;

完整mini-vue

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script>

function h(tag,props,children){
return {
tag,
props,
children
}
}
function mount(vnode,container){
//创建相应的真实DOM结点
const el=vnode.el=document.createElement(vnode.tag);
//props
if(vnode.props){
for(const key in vnode.props){
const value=vnode.props[key];
if(key.startsWith('on')){
el.addEventListener(key.slice(2).toLowerCase(),value)//事件机制
}else{
el.setAttribute(key,value);
}

}
}
//children
if(vnode.children){
if(typeof vnode.children==='string'){
el.textContent=vnode.children;
}
else{
vnode.children.forEach(child=>{
mount(child,el)//递归
})
}
}
container.appendChild(el);
}
function patch(n1,n2){
const el=n2.el=n1.el;
if(n1.tag===n2.tag){
//props
const oldProps=n1.props||{};
const newProps=n1.props||{};
for(const key in newProps){
const oldValue=oldProps[key];
const newValue=newProps[key]
if(newValue!==oldValue){
el.setAttribute(key,newValue)
}
}
for(const key in oldProps){
if(!key in newProps){
el.removeAttribute(key)
}
}
//children
const oldChildren=n1.children;
const newChildren=n2.children;
if(typeof newChildren==='string'){
if(typeof oldChildren==='string'){
if(newChildren!==oldChildren){
el.textContent=newChildren;
}
}else{
el.textContent=newChildren
}
}else {
//if newChildren is not string
if(typeof oldChildren==='string'){
el.innerHTML=''
newChildren.forEach(child=>{
mount(child,el)
})
}else{
const commonLength=Math.min(oldChildren.length,newChildren.length)
for(let i=0;i<commonLength;i++){
patch(oldChildren[i],newChildren[i])
}
if(newChildren.length>oldChildren.length){
newChildren.slice(oldChildren.length).forEach(child=>{
mount(child,el)
})

}else if(newChildren.length<oldChildren.length){
oldChildren.slice(newChildren.length).forEach(child=>{
el.removeChild(child.el);
})
}
}
}

}else{
//replace

}

}

let activeEffect;
class Dep{
subscribers=new Set()
depend(){
if(activeEffect){
this.subscribers.add(activeEffect)
}
}
notify(){
this.subscribers.forEach(effect=>{
effect()
})
}

}

const targetMap=new WeakMap()//只接受键值是对象,不接受其他类型的值作为键值,本身不可以从任何代码访问,而且会被自动垃圾回收,不可以迭代
function getDep(target,key){
let depsMap=targetMap.get(target)
if(!depsMap){
depsMap=new Map()//可以迭代键
targetMap.set(target,depsMap)
}
let dep=depsMap.get(key)
if(!dep){
dep=new Dep()
depsMap.set(key,dep)
}

return dep;

}
const reactiveHandler={
get(target,key,receiver){
let dep=getDep(target,key)
dep.depend()
return Reflect.get(target,key,receiver)//Reflect不会抛出异常,只会抛出真假
},
set(target,key,value,receiver){
let dep=getDep(target,key)
const result=Reflect.set(target,key,value,receiver)
dep.notify()
return result

}

}
function reactive(raw){
return new Proxy(raw,reactiveHandler)//Proxy 会触发set get 有利于数组观测,不必多写一些数组内置方法
}

function watchEffect(effect){
activeEffect=effect
effect()
activeEffect=null
}
const dep=new Dep()
const App={
data:reactive({
count:0
}),
render(){
return h('div',{
onClick:()=>{
this.data.count++;
}
},
String(this.data.count))
}
}
function mountApp(component,container){
let isMounted=false
let prevVdom
watchEffect(()=>{
if(!isMounted){
prevVdom=component.render()
mount(prevVdom,container)
isMounted=true
}else{
const newVdom=component.render()
patch(prevVdom,newVdom)
prevVdom=newVdom
}
})
}
mountApp(App,document.getElementById('app'))
</script>
</body>
</html>

初始化package.json

初始化package.json有两种方式,一种是通过npm管理,一种是通过yarn管理。

npm命令:

1
npm init

yarn命令:

1
yarn init

直接用默认配置:

1
yarn init -y

package.json中dependencies,devDependencies,peerDependencies,scripts这几个字段的意思。

dependencies:生产环境,项目运行的依赖(如ract,ract-dom

devDependencies开发环境,项目所需的依赖(webpack插件,打包插件叶索插件,eslint等)

peerDependencies 包不会自动安装,会提示你项目运行,需要主动安装该依赖

scripts命令脚本

引入TypeScript

1
npm i -D typescript

或者

1
yarn add typescript -D

用tsc命令初始化生产tsconfig.json文件

1
tsc --init

tssconfig.js

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
{
"compilerOptions": {
"rootDir": "./src",//源码目录
"target": "es5", // 指定输出 ECMAScript 目标版本
"module": "ESNext", //面向未来的ESM模块化
"strict": true, // 开启所有的严格检查配置
"esModuleInterop": true, // 允许 export = xxx 导出 ,并使用 import xxx form "module-name" 导入
"outDir": "dist",
/* 指定要包含在编译中的库文件——引用类库——即申明文件,如果输出的模块方式是 es5,就会默认引入 "dom","es5","scripthost" 。如果在 TS 中想要使用一些 ES6 以上版本的语法,就需要引入相关的类库 */
"lib": [
"webworker",
"dom",
"es5",
"es2015",
"es2016",
"es2015.promise",
"dom.iterable",
"scripthost",
"esnext",
], // 要包含在编译中的依赖库文件列表
"allowJs": true, // 允许编译 JavaScript 文件
// 检查 JS 文件
"checkJs": true,
"skipLibCheck": true, // 跳过所有声明文件的类型检查
"allowSyntheticDefaultImports": true, // 允许从没有默认导出的模块进行默认导入
"resolveJsonModule": true, // 允许使用 .json 扩展名导入的模块
/* react 模式下:直接将 JSX 编译成 JS,会生成 React.createElement 的形式,在使用前不需要再进行转换操作了,输出文件的扩展名为 .js */
/* preserve 模式下:不会将 JSX 编译成 JS,生成代码中会保留 JSX,以供后续的转换操作使用(比如:Babel)。 另外,输出文件会带有 .jsx 扩展名 */
/* react-native 模式下:相当于 preserve,它也保留了所有的 JSX,但是输出文件的扩展名是 .js */
"jsx": "react", // 在.tsx文件中支持JSX
"sourceMap": true, // 生成相应的.map文件
"declaration": true, // 生成相应的.d.ts文件
"allowUmdGlobalAccess": true,
"experimentalDecorators": true, // 启用对ES装饰器的实验性支持
"moduleResolution": "node", // 将模块解析模式设置为node.js解析模式
"baseUrl": "./",
"incremental": true, // 通过从以前的编译中读取/写入信息到磁盘上的文件来启用增量编译
"forceConsistentCasingInFileNames": true,
/* 当目标是ES5或ES3的时候提供对for-of、扩展运算符和解构赋值中对于迭代器的完整支持 */
"downlevelIteration": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
// 不允许使用隐式的 any 类型
"noImplicitAny": false,
// 不允许 this 有隐式的 any 类型,即 this 必须有明确的指向
"noImplicitThis": false,
// 不允许把 null、undefined 赋值给其他类型变量
"strictNullChecks": false,
"paths": {
//别名
"@/*": [
"src/*"
],
"@images/*": [
"src/assets/images/*"
],
}
},
"include": [
"src"
],
"exclude": [
"node_modules",
"dist"
] // *** 不进行类型检查的文件 ***
}


引入webpack

1
npm i webpack webpack-cli webpack-dev-server --save-dev

构建tsx

关于TS转JS,有三种方案

  1. tsc 缺点,转换为es5后,一些语法特性不能转换
  2. ts-loader
  3. babel-loader+@babel/preset-typescript 插件丰富,后序兼容扩展性强
1
npm i @babel/core @babel/preset-env babel-loader core-js

[babel中文网](https://link.juejin.cn/?target=https%3A%2F%2Fwww.babeljs.cn%2Fdocs%2Fusage

安装必要的webapck插件和Loader

1
npm i -D html-webpack-plugin less-loader css-loader style-loader postcss postcss-loader

配置webpack.config.js

解释:path.resolve()方法用于将相对路径转为绝对路径

它接收多个参数,依次表示所要进入的路径,直到将最后一个参数转为绝对路径,如果根据参数无法得到绝对路径,就以当前所在路径作为基准,除了根目录,该方法的返回值不带尾部斜杠

例如:

1
2
3
4
5
// 格式
path.resolve([from ...], to)

// 实例
path.resolve('foo/bar', '/tmp/file/', '..', 'a/../subfile')

执行效果:

1
2
3
4
5
$ cd foo/bar
$ cd /tmp/file/
$ cd ..
$ cd a/../subfile
$ pwd

node.js中的文件路径大概有____dirname,___filename,precess.cwd,./或者../

1
2
3
4
__dirname:    获得当前执行文件所在目录的完整目录名
__filename: 获得当前执行文件的带有完整绝对路径的文件名
process.cwd():获得当前执行node命令时候的文件夹目录名
./: 文件所在目录
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
const HtmlWebpackPlugin = require('html-webpack-plugin')
const path=require('path')

module.exports={
entry:'./src/index.ts',
//用来设置引用模块,避免找不到ts和js模块
resolve: {
extensions:['.ts','.js']

},
output:{
path:path.resolve(__dirname,'dist'),
//打包后文件
filename:"bundle.js",
clean:true,
environment:{
arrowFunction: false,//关闭webpack的箭头函数,可选
}

},

//开发模式使用,方便查错误
devtool:'inline-source-map',
devServer: {
static:'./dist',
},

module:{
rules:[
{
test:/\.ts$/,
use:
[
{
loader:"babel-loader",
//设置babel
options:{
//设置预定义环境
presets:[
[
//指定环境的插件
"@babel/preset-env",
//配置信息
{
//要兼容的目标浏览器
targets:{
chrome:"58",
ie:"11",
},
//指定corejs版本
corejs:'3',
//使用corejs的方式"usage"表示按需加载,能够解决ie11旧浏览器中promise无法使用的问题
useBuiltIns:"usage",

}
]

]
}

},
{
loader:'ts-loader',
},
],

exclude:/node-modules/
},
{
test:/\.css$/,
//使用哪些loader
use:[
//use数组中loader执行顺序从右到左,从下到上一次执行
//创建style标签,将js中的样式资源插入进行,添加到head中生效
'style-loader',
//将css文件变成commonjs模块加载到js中,里面内容是样式字符串
'css-loader'
]},
{
test:/\.less$/,
use:[
'style-loader',
'css-loader',
//引入postcss
{
loader:"postcss-loader",
options:{
postcssOptions:{
plugins:[
[
"postcss-preset-env",
{
browsers:'last 2 versions'
}
]
]
}
}

},
'less-loader',//将less文件编译成css文件
]
},

]
},
plugins:[
new HtmlWebpackPlugin(
{template:'./src/index.html'}
)
],
mode:'development'

}

修改 package.json中scripts

1
2
3
4
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack-dev-server --open"
},

直接npm run build就能启动webpack-dev-server

装饰器

_装饰器_是一种特殊类型的声明,它能够被附加到类声明方法访问符属性参数上。 装饰器使用@expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。

装饰器工厂

定制一个修饰器如何应用到一个声明上,得写一个装饰器工厂函数,装饰器工厂就是一个简单的函数,它返回一个表达式,以供装饰器在运行时调用

1
2
3
4
5
function color(value: string) { // 这是一个装饰器工厂
return function (target) { // 这是装饰器
// do something with "target" and "value"...
}
}

类装饰器

应用于类构造函数,可以用来监视,修改或替换类定义。类装饰器不能用在声明文件中(.d.ts),也不能用在人格化外部上下文中

类装饰器表达式会在运行是被当做函数被调用,类的构造函数作为其唯一的参数。

如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function logClass(params:string){
return function(target:any){
console.log(target);//类
console.log(params);//传入的参数
target.prototype.apiurl=params;
}
}
@logClass("http://123.com")*
class HttpClient {
constructor(){

}
gatData(){

}

}
var http:any=new HttpClient();

重载构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function logClass<T extends {new(...args:any[]):{}}>(constructor:T){
console.log(constructor);
return class extends constructor {
apiurl:any="我是修改后的apiurl";
getData(){
this.apiurl=this.apiurl+'!!';
console.log(this.apiurl);
}
}
}
@logClass
class HttpClient {
public apiurl:string| undefined;
constructor(){
this.apiurl='我是构造函数里的apiurl';
}
getData(){
console.log(this.apiurl);
}
}
let http:any=new HttpClient();
http.getData();

方法装饰器

声明在一个方法的声明之前(紧靠着方法声明),他会被应用到方法的属性描述符上,可以用来监视,修改或者替换方法定义。方法装饰器不能用在声明文件(.d.ts),重载或者任何外部上下文中

方法装饰器表达式会在运行时当做函数被调用,传入下列3个参数:

1 对静态成员来说是类的构造函数,对于实力成员来说是类的原型对象

2 方法的名字

3 方法的属性描述符

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
33
34
35
36
37
38
39
function get(params:any){
return function(target:any,methodName:any,desc:any){
console.log(target);//类的原型对象
console.log(methodName)//getData
console.log(desc.value)
//ƒ getData(...args) {
// console.log(args);
//console.log.toString("我是getData里面的方法");
//}
//修改方法装饰器,改为可以传入参数
//保存当前方法
let oMethod=desc.value;
desc.value=function(...args:any[]){
args.map((val)=>{
return String(val);
})
console.log(this);//类
oMethod.apply(this,args);
}



}

}
class HttpClient {
url: any | undefined;
constructor(){

}
@get('123')
getData(...args:any[]){
console.log(args);
console.log.toString("我是getData里面的方法")
}

}
let http=new HttpClient();
http.getData('123',456);

属性装饰器

声明在一个属性声明之前,属性装饰器不能用纸声明文件中(.d.ts),或者任何外部上下文(比如declare的类)里

属性装饰器表达式会在运行时被当做函数被调用,传入下列2个参数:

1 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象

2 属性的名字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function logProperty(params:any){
return function (target:any,attr:any){
console.log(target);//类的原型对象
console.log(attr);//url
target[attr]=params;
}
}
class HttpClient {
@logProperty('http://123.com')
url:any | undefined;
constructor(){
}
getData(){
console.log(this.url);
}
}
var http=new HttpClient();
http.getData();

参数装饰器

参数装饰器_声明在一个参数声明之前(紧靠着参数声明)。 参数装饰器应用于类构造函数或方法声明。 参数装饰器不能用在声明文件(.d.ts),重载或其它外部上下文(比如declare的类)里。

参数装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  3. 参数在函数参数列表中的索引

装饰器作用:扩展类的方法和属性

装饰器执行顺序:

属性>方法>方法参数>类

多个同样的装饰器,执行顺序由后往前