ZEROMAKE | keep codeing and thinking!
2017-07-26 | source

preact 源码解读(2)

前言

  • 这里是第二篇,第一篇在这里
  • 这次讲 Component,以及它的一些轻量依赖。
  • 顺便说下司徒正美的 preact 源码学习
  • 感觉比我写的好多了,图文并茂,还能提出和其它如 React 的源码比较。
  • 我唯一好点的可能就是代码几乎每行都有注释,并且使用了 typescript 添加了类型的标注。

Component 使用

1
import { h, Component, render } from "preact"
2
3
class App extends Component {
4
constructor(props, context) {
5
super(props, context)
6
this.state = {
7
num: 0
8
}
9
}
10
test() {
11
this.setState(state => {
12
state.num += 1
13
})
14
}
15
render(props, state, context) {
16
return <h1 onClick={test.bind(this)}>{state.num}<h1/>
17
}
18
}
19
render(<App/>, document.body)

上面是一个简单的点击改变当前状态的组件示例。
其中与vue不同preact通过Component.prototype.setState来触发新的 dom 改变。
当然preact还有其它的更新方式。

Component 代码

这里的代码是通过typescript重写过的所以有所不同,
但是更好的了解一个完整的Component整体应该有什么。

1
import { FORCE_RENDER } from "./constants";
2
import { renderComponent } from "./vdom/component";
3
import { VNode } from "./vnode";
4
import { enqueueRender } from "./render-queue";
5
import { extend } from "./util";
6
import { IKeyValue } from "./types";
7
8
export class Component {
9
/**
10
* 默认props
11
*/
12
public static defaultProps?: IKeyValue;
13
/**
14
* 当前组件的状态,可以修改
15
*/
16
public state: IKeyValue;
17
/**
18
* 由父级组件传递的状态,不可修改
19
*/
20
public props: IKeyValue;
21
/**
22
* 组件上下文,由父组件传递
23
*/
24
public context: IKeyValue;
25
/**
26
* 组件挂载后的dom
27
*/
28
public base?: Element;
29
/**
30
* 自定义组件名
31
*/
32
public name?: string;
33
/**
34
* 上一次的属性
35
*/
36
public prevProps?: IKeyValue;
37
/**
38
* 上一次的状态
39
*/
40
public prevState?: IKeyValue;
41
/**
42
* 上一次的上下文
43
*/
44
public prevContext?: IKeyValue;
45
/**
46
* 被移除时的dom缓存
47
*/
48
public nextBase?: Element;
49
/**
50
* 在一个组件被渲染到 DOM 之前
51
*/
52
public componentWillMount?() => void;
53
/**
54
* 在一个组件被渲染到 DOM 之后
55
*/
56
public componentDidMount?() => void;
57
/**
58
* 在一个组件在 DOM 中被清除之前
59
*/
60
public componentWillUnmount?() => void;
61
/**
62
* 在新的 props 被接受之前
63
* @param { IKeyValue } nextProps
64
* @param { IKeyValue } nextContext
65
*/
66
public componentWillReceiveProps?(nextProps: IKeyValue, nextContext: IKeyValue) => void;
67
/**
68
* 在 render() 之前. 若返回 false,则跳过 render,与 componentWillUpdate 互斥
69
* @param { IKeyValue } nextProps
70
* @param { IKeyValue } nextState
71
* @param { IKeyValue } nextContext
72
* @returns { boolean }
73
*/
74
public shouldComponentUpdate?(nextProps: IKeyValue, nextState: IKeyValue, nextContext: IKeyValue) => boolean;
75
/**
76
* 在 render() 之前,与 shouldComponentUpdate 互斥
77
* @param { IKeyValue } nextProps
78
* @param { IKeyValue } nextState
79
* @param { IKeyValue } nextContext
80
*/
81
public componentWillUpdate?(nextProps: IKeyValue, nextState: IKeyValue, nextContext: IKeyValue) => void;
82
/**
83
* 在 render() 之后
84
* @param { IKeyValue } previousProps
85
* @param { IKeyValue } previousState
86
* @param { IKeyValue } previousContext
87
*/
88
public componentDidUpdate?(previousProps: IKeyValue, previousState: IKeyValue, previousContext: IKeyValue) => void;
89
/**
90
* 获取上下文,会被传递到所有的子组件
91
*/
92
public getChildContext?() => IKeyValue;
93
/**
94
* 子组件
95
*/
96
public _component?: Component;
97
/**
98
* 父组件
99
*/
100
public _parentComponent?: Component;
101
/**
102
* 是否加入更新队列
103
*/
104
public _dirty: boolean;
105
/**
106
* render 执行完后的回调队列
107
*/
108
public _renderCallbacks?: any[];
109
/**
110
* 当前组件的key用于复用
111
*/
112
public _key?: string;
113
/**
114
* 是否停用
115
*/
116
public _disable?: boolean;
117
/**
118
* react标准用于设置component实例
119
*/
120
public _ref?: (component: Component | null) => void;
121
/**
122
* VDom暂定用于存放组件根dom的上下文
123
*/
124
public child?: any;
125
constructor(props: IKeyValue, context: IKeyValue) {
126
// 初始化为true
127
this._dirty = true;
128
this.context = context;
129
this.props = props;
130
this.state = this.state || {};
131
}
132
/**
133
* 设置state并通过enqueueRender异步更新dom
134
* @param state 对象或方法
135
* @param callback render执行完后的回调。
136
*/
137
public setState(state: IKeyValue, callback?: () => void): void {
138
const s: IKeyValue = this.state;
139
if (!this.prevState) {
140
// 把旧的状态保存起来
141
this.prevState = extend({}, s);
142
}
143
// 把新的state和并到this.state
144
if (typeof state === "function") {
145
const newState = state(s, this.props);
146
if (newState) {
147
extend(s, newState);
148
}
149
} else {
150
extend(s, state);
151
}
152
if (callback) {
153
// 添加回调
154
this._renderCallbacks = this._renderCallbacks || [];
155
this._renderCallbacks.push(callback);
156
}
157
// 异步队列更新dom,通过enqueueRender方法可以保证在一个任务栈下多次setState但是只会发生一次render
158
enqueueRender(this);
159
}
160
/**
161
* 手动的同步更新dom
162
* @param callback 回调
163
*/
164
public forceUpdate(callback: () => void) {
165
if (callback) {
166
this._renderCallbacks = this._renderCallbacks || [];
167
this._renderCallbacks.push(callback);
168
}
169
// 重新同步执行render
170
renderComponent(this, FORCE_RENDER);
171
}
172
/**
173
* 用来生成VNode的函数
174
* @param props
175
* @param state
176
* @param context
177
*/
178
public render(props?: IKeyValue, state?: IKeyValue, context?: IKeyValue): VNode | void {
179
// console.error("not set render");
180
}
181
}

如果你看过原来的preact的代码会发觉多了很多可选属性,
其中除了child这个属性其它实际上官方的也有,但是都是可选属性。

这里重点说setStateforceUpdate这两个触发 dom 更新

setState保存旧的this.statethis.prevState里,然后新的 state 是直接设置在this.state
然后通过enqueueRender来加入队列中,这个更新是在异步中的。所以不要写出这种代码

1
test() {
2
// 这里的setState已经入异步栈,
3
this.setState({...})
4
$.post(...() => {
5
// 再次入异步栈,再一次执行,
6
this.setState({...})
7
})
8
}

可以把两次setState合并到一起做。

render-queue

1
import { Component } from "./component";
2
import options from "./options";
3
import { defer } from "./util";
4
import { renderComponent } from "./vdom/component";
5
6
let items: Component[] = [];
7
8
/**
9
* 把Component放入队列中等待更新
10
* @param component 组件
11
*/
12
export function enqueueRender(component: Component) {
13
if (!component._dirty) {
14
// 防止多次render
15
component._dirty = true;
16
const len = items.push(component);
17
if (len === 1) {
18
// 在第一次时添加一个异步render,保证同步代码执行完只有一个异步render。
19
const deferFun = options.debounceRendering || defer;
20
deferFun(rerender);
21
}
22
}
23
}
24
25
/**
26
* 根据Component队列更新dom。
27
* 可以setState后直接执行这个方法强制同步更新dom
28
*/
29
export function rerender() {
30
let p: Component | undefined;
31
const list = items;
32
items = [];
33
while ((p = list.pop())) {
34
if (p._dirty) {
35
// 防止多次render。
36
renderComponent(p);
37
}
38
}
39
}

最终通过renderComponent来重新diff更新dom

forceUpdate则是直接同步更新不过传入了一个标记FORCE_RENDER

顺便写下options

1
import { VNode } from "./vnode";
2
import { Component } from "component";
3
4
const options: {
5
// render更新后钩子比componentDidUpdate更后面执行
6
afterUpdate?: (component: Component) => void;
7
// dom卸载载前钩子比componentWillUnmount更先执行
8
beforeUnmount?: (component: Component) => void;
9
// dom挂载后钩子比componentDidMount更先执行
10
afterMount?: (component: Component) => void;
11
// setComponentProps时强制为同步render
12
syncComponentUpdates?: boolean;
13
// 自定义异步调度方法,会异步执行传入的方法
14
debounceRendering?: (render: () => void) => void;
15
// vnode实例创建时的钩子
16
vnode?: (vnode: VNode) => void;
17
// 事件钩子,可以对event过滤返回的会代替event参数
18
event?: (event: Event) => any;
19
// 是否自动对事件方法绑定this为组件,默认为true(preact没有)
20
eventBind?: boolean;
21
} = {
22
eventBind: true
23
};
24
25
export default options;

后记

  • 感觉有了更多的注释,就没有必要说明太多了。
  • 下一篇应该是到了renderComponentdiff部分了。