系列文章:
本文大抵是《从 Preact 源码一窥 React 原理》系列的收尾篇了,算上这一篇的组件原理 Preact 的核心源码中的部分也已经基本涵盖了。Preact 中的诸多细节自然是无法面面俱到的,还请诸位自行阅读罢。
剩余还有 Hook 机制、Preact 新增的 Linked State 特性等等,由于其未被 Preact 的核心所包含,因此可能会放在后续进行介绍(有缘再见吧 )。
React 组件是一部分独立的、且可复用的 UI 单元,根据应用方式的不同可以分为类组件以及函数组件。
以下是 Preact 中组件渲染的一个示例:
import { h, render, Component } from 'preact';
// 类组件
class Clock extends Component {
render() {
let time = new Date();
return <time datetime={time.toISOString()}>{ time.toLocaleTimeString() }</time>;
}
}
// 函数组件
function Clock () {
let time = new Date();
return <time datetime={time.toISOString()}>{ time.toLocaleTimeString() }</time>;
}
// render an instance of Clock into :
render(<Clock />, document.body);
如果你曾使用过 React 进行开发,那么你对上述的代码应当很熟悉了。让我们首先着眼于类组件,可以看到,所有的类组件都必须由 Component
基类派生而来。
这也就引出了我们所讨论的第一个重点:Component
类。
Preact 中 Component
类的定义没有使用 ES6 语法的 class
(尽管在其他代码中使用了 ES6 的相关语法),而是使用了 ES5 的方式。
由于 JavaScript 的灵活性,Component
中的属性在构造函数以外动态添加,因此 Preact 贴心的为 Component
中的几个属性提供了注释,其代码如下所示:
export function Component(props, context) {
this.props = props;
this.context = context;
// this.constructor // When component is functional component, this is reset to functional component
// if (this.state==null) this.state = {};
// this.state = {};
// this._dirty = true;
// this._renderCallbacks = []; // Only class components
// Other properties that Component will have set later,
// shown here as commented out for quick reference
// this.base = null;
// this._context = null;
// this._ancestorComponent = null; // Always set right after instantiation
// this._vnode = null;
// this._nextState = null; // Only class components
// this._prevVNode = null;
// this._processingException = null; // Always read, set only when handling error
}
其中,各个属性值各自的含义为:
props
:组件接收的 props 值;context
:组件获得的 context 值,根据其使用的 API 不同可能是 Legacy Context 的值或是 New Context 的值;state
:组件内部状态;_dirty
:标记组件内部状态是否发生改变,也即是组件是否需要进行重新 Diff;_renderCallbacks
:组件渲染完成的回调函数;base
:调用 render
函数渲染之后得到的真实 DOM;_context
:保存了自父组件获得的 Legacy Context 值;_ancestorComponent
:最近的父组件;_vnode
:组件实例所对应的 VNode;_nextState
:在 Diff 执行前,暂时保存组件 state 变化后的值;_prevVNode
:使用 render
函数之后得到的 VNode
;_processingExecption
:需要由组件实例处理的上抛的错误;_depth
:在 Component
类中未提及,但在 diff
函数中被使用,代表组件的深度;_parentDom
:同样在 Component
类中未提及,但在 diff
函数中被使用,代表组件的父真实 DOM。在上文《从 Preact 源码一窥 React 原理(二):Diff 算法》中我们了解了 Diff 算法的原理以及具体实现,但是略过了 diff
函数中关于组件的部分代码。现在我们就将这部分代码补上:
export function diff(dom, parentDom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts, ancestorComponent, force, oldDom) {
// If the previous type doesn't match the new type we drop the whole subtree
// 如果 oldVnode 与 newVnode 的类型或者 key 值不匹配则将整个原先的 DOM 树抛弃
if (oldVNode==null || newVNode==null || oldVNode.type!==newVNode.type || oldVNode.key!==newVNode.key) {
if (oldVNode!=null) unmount(oldVNode, ancestorComponent);
if (newVNode==null) return null;
dom = null;
oldVNode = EMPTY_OBJ;
}
if (options.diff) options.diff(newVNode);
let c, p, isNew = false, oldProps, oldState, snapshot,
newType = newVNode.type;
/** @type {import('../internal').Component | null} */
let clearProcessingException;
try {
// outer: 是标签语法,较为少见,详见 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/label
// VNode 类型为 Fragment ,其为子节点的聚合,无需向 DOM 添加额外节点
outer: if (oldVNode.type===Fragment || newType===Fragment) {
balabala...
}
// VNode 类型为类组件或者函数组件
else if (typeof newType==='function') {
// PART 1
// 判断组件是否使用了 New Context API,当使用了 New Context API 则从 provider 获取 context 值,
// 否则从 diff 函数的参数中获取从父组件传递的 context 值。
let cxType = newType.contextType;
let provider = cxType && context[cxType._id];
let cctx = cxType != null ? (provider ? provider.props.value : cxType._defaultValue) : context;
// PART 2
// 当 oldVNode 中已经存在 Component 的实例时,将该实例赋值给 newVNode
if (oldVNode._component) {
// 将 oldVNode 的 _component,_dom 赋给 newVNode
c = newVNode._component = oldVNode._component;
clearProcessingException = c._processingException;
dom = newVNode._dom = oldVNode._dom;
}
// PART 3
// oldVNode 中不存在 Component 实例时,则为其创建一个
else {
// 分别通过类组件以及函数组件的方式创建组件的实例
if (newType.prototype && newType.prototype.render) {
newVNode._component = c = new newType(newVNode.props, cctx); // eslint-disable-line new-cap
}
else {
newVNode._component = c = new Component(newVNode.props, cctx);
// 对于函数组件,需要将创建实例的 constructor 指向函数
c.constructor = newType;
// doRender 函数也就是调用 this.constructor
c.render = doRender;
}
c._ancestorComponent = ancestorComponent;
// 对于使用了 New Context API 的组件(存在 provider),需要调用 sub 函数订阅 context 的更新
if (provider) provider.sub(c);
// 为新创建的组件实例设置一些初值
c.props = newVNode.props;
if (!c.state) c.state = {};
// 注意:这里可以看到 Component 中的 context 属性为组件使用的 context 值,其可能为 Legacy Context 或是 New Context,
// 而 _context 属性为 Legacy Context 的值,保存这一值的目的是在组件进行更新时,需要从组件中获得 Context 的值,即使其本身并不需要该值。
// 这也展示了 Legacy Context API 的缺陷:本质而言,其与 props 相同,仍是沿着树依次向下传递的。
c.context = cctx;
c._context = context;
isNew = c._dirty = true;
c._renderCallbacks = [];
}
// 更新 _vnode 值
c._vnode = newVNode;
// PART 4
// 调用新的生命周期 getDerivedStateFromProps 更新 state
// 相关资料参见:https://reactjs.org/docs/react-component.html
let s = c._nextState || c.state;
if (newType.getDerivedStateFromProps!=null) {
oldState = assign({}, c.state);
// 改成这样比较清晰:if (s===c.state) s = c._nextState = assign({}, c.state);
if (s===c.state) s = c._nextState = assign({}, s);
assign(s, newType.getDerivedStateFromProps(newVNode.props, s));
}
// PART 5
// 调用预渲染的生命周期方法
if (isNew) {
if (newType.getDerivedStateFromProps==null && c.componentWillMount!=null) c.componentWillMount();
// 将需要调用 componentDidMount 的组件暂存入 mounts 中,等待 diff 结束后通过 commitRoot 进行调用
if (c.componentDidMount!=null) mounts.push(c);
}
else {
// 如果不存在 getDerivedStateFromProps 时,则调用 componentWillReceiveProps
if (newType.getDerivedStateFromProps==null && force==null && c.componentWillReceiveProps!=null) {
c.componentWillReceiveProps(newVNode.props, cctx);
s = c._nextState || c.state;
}
// 通过 force 和 shouldComponentUpdate 来判断是否执行更新
if (!force && c.shouldComponentUpdate!=null && c.shouldComponentUpdate(newVNode.props, s, cctx)===false) {
// 如果 shouldComponentUpdate 返回 false 则执行一些赋值操作后直接结束主逻辑 (break outer)
dom = newVNode._dom;
c.props = newVNode.props;
c.state = s;
c._dirty = false;
newVNode._lastDomChild = oldVNode._lastDomChild;
break outer;
}
if (c.componentWillUpdate!=null) {
c.componentWillUpdate(newVNode.props, s, cctx);
}
}
// PART 6
// 保存下 oldProps 和 oldState,其会在后续的一些钩子函数或者生命周期函数中被使用
oldProps = c.props;
if (!oldState) oldState = c.state;
// 更新组件的各个属性
c.context = cctx;
c.props = newVNode.props;
c.state = s;
if (options.render) options.render(newVNode);
// PART 7
// 调用组件的 render 函数得到 vnode
let prev = c._prevVNode;
let vnode = c._prevVNode = coerceToVNode(c.render(c.props, c.state, c.context));
c._dirty = false;
// PART 8
// getChildContext 为 Legacy Context API ,如果其存在,则通过其更新 context 的值
// 需要注意的是,这里被更新的为 context 而不是 cctx 也就是说 Legacy Context 和 New Context 各管各,不会混在一块
if (c.getChildContext!=null) {
context = assign(assign({}, context), c.getChildContext());
}
// PART 9
// getSnapshotBeforeUpdate 同样是新的生命周期
// 相关资料参见:https://reactjs.org/docs/react-component.html
if (!isNew && c.getSnapshotBeforeUpdate!=null) {
snapshot = c.getSnapshotBeforeUpdate(oldProps, oldState);
}
// 更新组件深度
c._depth = ancestorComponent ? (ancestorComponent._depth || 0) + 1 : 0;
// 对 render 返回的 VNode 执行 diff
c.base = dom = diff(dom, parentDom, vnode, prev, context, isSvg, excessDomChildren, mounts, c, null, oldDom);
if (vnode!=null) {
// 如果返回 vnode 为 Fragment,那么其 _lastDomChild 非空,此时将其赋值给 newVNode
newVNode._lastDomChild = vnode._lastDomChild;
}
c._parentDom = parentDom;
// 更新 ref
if (newVNode.ref) applyRef(newVNode.ref, c, ancestorComponent);
}
// VNode 类型为元素节点
else { balabala... }
newVNode._dom = dom;
// PART 10
// 完成 diff 之后,调用 _renderCallbacks 中的回调函数以及 componentDidUpdate 生命周期
if (c!=null) {
while (p=c._renderCallbacks.pop()) p.call(c);
// Don't call componentDidUpdate on mount or when we bailed out via
// `shouldComponentUpdate`
if (!isNew && oldProps!=null && c.componentDidUpdate!=null) {
c.componentDidUpdate(oldProps, oldState, snapshot);
}
}
if (clearProcessingException) {
c._processingException = null;
}
if (options.diffed) options.diffed(newVNode);
}
catch (e) {
catchErrorInComponent(e, ancestorComponent);
}
return dom;
}
function doRender(props, state, context) {
// 直接调用函数组件的 constructor 也即是对应的函数
return this.constructor(props, context);
}
如果你阅读了前一篇文章《从 Preact 源码一窥 React 原理(二):Diff 算法》那么你应当对于 diff
函数的核心思路和实现逻辑很清楚了,这里就不再赘述。
上述代码中略去了已在前文中讨论的一些部分,让我们直接进入组件处理的正题:
newType.contextType
判断当前的使用 Context API 类型,并获取相应的 Context 值,具体内容将在 Context 一节中介绍;oldVNode
中存在组件的实例时,将该组件的实例赋值给 newVNode
;oldVNode
中存在组件的实例时,则为 newVNode
创建一个组件实例。new
关键字创建对应类的实例;如果为函数组件则创建一个 Component
的实例,并将其 constructor
设为该函数,实例的 render
方法设为 doRender
函数,其也即是直接调用对应的函数。provider
调用 sub
函数以订阅 context
的变化。state
、 _dirty
等;getDerivedStateFromProps
对组件的 state
进行更新,关于该生命周期的介绍请参见 React - Component;componentWillMount
,并将该组件放入 mounts
数组中,等待 Diff 完成后通过 commitRoot
函数调用对应的 componentDidMount
。componentWillReceiveProps
并通过 force
以及 shouldComponentUpdate
判断组件是否执行更新操作;oldProps
以及 oldState
从而后续的一些钩子函数或者生命周期函数中使用,并对组件中的各个属性进行更新;render
函数,获取其生成的 VNode
;getChildContext
,那么调用其更新父组件传递下来的 Context 值;getSnapshotBeforeUpdate
并执行 diff
函数对 render
函数生成的 VNode
进行更新。完成之后最后对组件一些辅助的值进行更新,例如 base
,_parentDom
等;_renderCallbacks
中的回调函数进行调用,并调用生命周期函数 componentDidUpdate
,完成渲染过程。组件的渲染过程介绍的较细,并且对每一段代码均附上了相应的注释,理解起来应当较为容易。
其中还有一部分内容未介绍的非常详尽,也就是 Context API 相关的代码,这一内容将在 Context API 一节中进行分析。
上一节内容介绍了如何对组件进行渲染,但是如果想要对组件执行更新应当如何做呢?
此时就需要通过 Component
类所提供的两个函数来完成相应的工作,其分别为:forceUpdate
函数和 setState
函数。
开发者可以通过调用 forceUpdate
函数对组件进行强制更新,强制更新的本质就在于通过 force
参数来取消 shouldComponentUpdate
的判断。事实上,同样可以通过 forceUpdate
进行非强制的更新,只需要将其参数设置为 false
即可。
以下是 forceUpdate
的实现:
Component.prototype.forceUpdate = function(callback) {
let vnode = this._vnode, dom = this._vnode._dom, parentDom = this._parentDom;
if (parentDom) {
// 如果 callback === false 则为非强制更新
const force = callback!==false;
// 与 render 函数相同,均是先调用 diff 执行更新后调用 commitRoot 执行 componentDidMount 回调
let mounts = [];
dom = diff(dom, parentDom, vnode, vnode, this._context, parentDom.ownerSVGElement!==undefined, null, mounts, this._ancestorComponent, force, dom);
if (dom!=null && dom.parentNode!==parentDom) {
parentDom.appendChild(dom);
}
commitRoot(mounts, vnode);
}
if (callback) callback();
};
forceUpdate
函数的实现较为简单,其与 render
函数想类似,均是先调用 diff
函数执行更新后调用 commitRoot
函数执行 componentDidMount
回调。
除了通过调用 forceUpdate
来强制更新组件,开发者还可以调用 setState
函数对组件的 state
进行修改。其随后会以异步的方式对组件进行更新。
setState
函数实现如下:
Component.prototype.setState = function(update, callback) {
// 当 _nextState 指向 state 时会对 state 进行一次拷贝,以防止更新过程影响当前 state
let s = (this._nextState!==this.state && this._nextState) || (this._nextState = assign({}, this.state));
// update 可以是函数或者对象
if (typeof update!=='function' || (update = update(s, this.props))) {
// 通过 assign 对 _nextState 进行更新
assign(s, update);
}
// update 的函数返回值为 null 则不需要执行组件更新
if (update==null) return;
// 当组件需要更新时,将其放入待更新队列中
if (this._vnode) {
if (callback) this._renderCallbacks.push(callback);
enqueueRender(this);
}
};
如果组件当前的 _nextState
属性指向 state
属性的值,则 setState
函数会首先对 state
进行一次拷贝。后续的多次 setState
操作均会在拷贝后的 _nextState
值上进行更新。
传入 setState
函数的参数 update
可以是函数或者是对象,其均会通过 assign
对 _nextState
进行更新。如果 update
函数的返回值为 null
则说明该操作不需要执行后续的 diff
;否则,将对当前组件调用 enqueueRender
函数,将当前组件放入待更新队列中。
enqueueRender
函数的相关实现如下:
export function enqueueRender(c) {
// 设置组件的 _dirty 为 true 并将组件加入待更新队列中
// 如果待更新队列长度为 1 则通过 defer 延迟调用 process
if (!c._dirty && (c._dirty = true) && q.push(c) === 1) {
(options.debounceRendering || defer)(process);
}
}
// defer 可以通过 Promise 或者 setTimeout 实现
const defer = typeof Promise=='function' ? Promise.prototype.then.bind(Promise.resolve()) : setTimeout;
function process() {
let p;
// 首先根据深度对队列中的组件进排序,首先更新深度较高的组价
q.sort((a, b) => b._depth - a._depth);
while ((p=q.pop())) {
// 通过调用 forceUpdate 来实现更新,此时参数为 false 也即是非强制更新
if (p._dirty) p.forceUpdate(false);
}
}
enqueueRender
函数将组件的 _dirty
值设为 true
,并将其放入待更新的队列中。如果该队列中仅有一个值,则会通过 defer
函数延迟调用 process
函数。
defer
函数通过 Promise
或者 setTimeout
将函数推迟到下一个 Tick 执行。
process
函数首先对队列中的组件进行排序,并按照深度从高到低的顺序调用 forceUpdate
进行组件的更新。
上述的三个函数也就构成了 Preact 所实现的批量更新特性。在一个 Tick 内所有因 setState
而改变了状态,需要执行更新的组件均会被待更新队列存储下来,并在下一个 Tick 中一起更新。
在 Context API 的实现原理之前,首先做一些简单的概念铺垫。如果您想要进一步理解 Legacy Context API 和 New Context API ,请参阅 React 的官方文档 React - Context 。
Context API 是什么?
Context 通过组件树提供了一个传递数据的方法,从而避免了在每一个层级手动的传递 props 属性。
来自 React - Context
当你需要在两个层次深度间隔很大的组件之间传递数据时,可以使用连续传递 props 来实现这一需求,但是一旦需求改变,就需要开发者修改每一个中间组件,这样的开发代价是不可接受的。
为此,React 提供了 Context API 可以在任意层级深度间隔的组件之间传递需要的数据。
在 16.x 版本的 React 中,React 提供了两种 Context API:
Legacy Context 也即是 React 16.x 前所一直提供的 Context API。
虽然 Legacy Context 被例如 Redux 等第三方库所广泛使用,但是事实上其一直仅仅是实验性的 API。目前其仍然存在 React 中,但在未来的版本中将会被移除。
既然 React / Preact 目前仍然支持这一 API ,那么我们还是有必要了解一下这一 API。
事实上,Legacy Context 在 Preact 中的实现与 Props 类似(在 React 中也是同样),均是沿着树状结构依次向下传递。
首先需要关注的是 diff
、diffChildren
两个函数的参数,这两个函数的相互调用构成了的 Preact 中 Diff 算法的主体:
function diff(dom, parentDom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts, ancestorComponent, force, oldDom)
function diffChildren(parentDom, newParentVNode, oldParentVNode, context, isSvg, excessDomChildren, mounts, ancestorComponent, oldDom)
可以看到两个函数中均包含了 context
参数,正如之前所提,这一参数也就是 Legacy Context API 所创建的 Context 值。
Legacy Context 值的源头在于 getChildContex
函数,其在 diff
函数的 PART 8 中被调用,见如下代码:
if (c.getChildContext!=null) {
context = assign(assign({}, context), c.getChildContext());
}
如果组件申明了 getChildContex
函数,则将其添加到 当前 Legacy Context 的拷贝值之上,生成新的 Legacy Context 并继续向下传递。
Legacy Context 值的获取从 diff
函数的参数中来。在 diff
函数的 PART 1 中,函数会判断当前组件所使用的 Context API 类型,如果是 Legacy Context 则将其赋值给 cctx
,其也就是组件实际所使用的 Context 值。
无论组件是否需要使用 Context 值,其内部也会由 _context
属性保存当前组件所接收的 Legacy Context 值,从而使得组件在通过 setState
或者 forceUpdate
进行更新时能够为子组件传递 Legacy Context 的值。
Preact 中对于 Context 相关变量的声明很不清晰,如果将变量名做如下更改则会更易阅读:
New Context API 是 React 在 16.x 版本中所推出的新 Context API。与 Legacy Context API 不同,其不再是实验性质的 API ,而是稳定的受到良好支持的 API。
关于 New Context 的介绍在 React 官方文档中介绍的非常详细,其核心就在于 Provider - 提供者以及 Customer - 消费者的概念(具体内容请参阅 React - Context)。
New Context 中的 Provider 以及 Customer 均需要通过入口函数 createContext
进行创建,其也是 New Context API 实现的核心内容,其代码如下:
export function createContext(defaultValue) {
const id = '__cC' + i++;
// context 中保存的是唯一的 id 以及默认值
let context = {
_id: id,
_defaultValue: defaultValue
};
// Consumer 的渲染不额外创建 node ,直接将子结果进行渲染,同时传递一个 context 参数
function Consumer(props, context) {
// 注意,这里的 children 必然为一个函数
return props.children(context);
}
Consumer.contextType = context;
context.Consumer = Consumer;
let ctx = { [id]: null };
// comp 指的是 Provider 实例化后得到的组件实例
function initProvider(comp) {
// 已订阅的消费者组件
const subs = [];
// 用于在 Consumer 中取到 Provider
comp.getChildContext = () => {
ctx[id] = comp;
return ctx;
};
// 每次 shouldComponentUpdate 被调用时,均会通知所有已订阅的消费者,对其进行更新
comp.shouldComponentUpdate = props => {
subs.map(c => {
// New Context 的值实际上仍需要从 Provider 的 props.value 上取
if (c._parentDom) {
c.context = props.value;
enqueueRender(c);
}
});
};
// 将消费者组件订阅到数组中
comp.sub = (c) => {
subs.push(c);
let old = c.componentWillUnmount;
// 当消费者卸载时,同时在订阅中删除对应组件
c.componentWillUnmount = () => {
subs.splice(subs.indexOf(c), 1);
old && old();
};
};
}
// 只有 Provider 组件渲染了才会初始化 provider
function Provider(props) {
if (!this.getChildContext) initProvider(this);
return props.children;
}
context.Provider = Provider;
return context;
}
createContext
中的几个核心部分分别为:
id
:用作标识的唯一 ID;context
:保存了 ID 信息以及默认的 Context 值,需要注意的是其并不是 New Context 的值;ctx
:保存了对 Provider
组件的索引,其同样并不是 New Context 的值;Customer
:消费者组件只是简单的对 children
的函数进行执行;Provider
:当 Provider
组件渲染时,其会执行对该组件的初始化操作,为其添加三个函数。getChildContext
函数用于将 ctx
的值添加到 Legacy Context 中,从而使得 Customer
能够获取 Provider
的实例;shouldComponentUpdate
生命周期函数会在 New Context 的值更新时,通知所有订阅的 Customer
,另其执行更新操作;sub
函数会将 Customer
订阅到 Provider
上,并截获其 componentWillUnmount
生命周期,当 Customer
卸载时,会取消其订阅。事实上,虽然 createContext
的实现中存在名为 context
、ctx
的变量,但是实际上其均不是 New Context 的值。真正被使用的 New Context 值仍需要从 Provider
的 props 中取到。
让我们再回过头来看 diff
函数中的相关实现,其 PART 1 代码如下所示:
// 判断组件是否使用了 New Context API,当使用了 New Context API 则从 provider 获取 context 值,
// 否则从 diff 函数的参数中获取从父组件传递的 context 值。
let cxType = newType.contextType;
let provider = cxType && context[cxType._id];
let cctx = cxType != null ? (provider ? provider.props.value : cxType._defaultValue) : context;
现在来看这段代码就更为明白了:通过 newType.contextType
可以判断当前组件是否为 Customer
,如果是,则通过合并到 Legacy Context 上的 ctx 来获取 Provider
的实例。得到 Provider
实例之后,也就能获取到 provider.props.value
的值,也即是 New Context 的值。
这一一来,我们就能理清 New Context 的工作流程:
createContext
创建 Provider
以及 Customer
;Provider
组件渲染,调用 initProvider
执行初始化操作,通过 getChildContext
函数将 ctx
也即是对 Provider
实例的引用放入Legacy Context 中;Customer
组件渲染,通过 Legacy Context 获取 Provider
的实例,从而获取 New Context 值,并调用 sub
函数保持订阅;Provider
组件 props.value
更新,通过生命周期函数 shouldComponentUpdate
通知所有 Customer
进行更新;Customer
组件卸载时,通过被 Provider
截获的生命周期函数 componentWillUnmount
取消订阅。顺便一提,可以看出在 Preact 中 New Context 在 Preact 中的实现也依赖于通过 Legacy Context 来获取 Provider
的实例。这样的方式并不优雅,尤其是在 Legacy Context 将会在未来被废弃的情况下。Preact 的开发人员或许需要重新考虑 New Context 的实现方式了。
本文分析了 Preact 组件相关的具体实现,至此,Preact 核心部分的分析已经基本涵盖。
虽然囿于水平,无法面面俱到,但也算是费心费力整理的干货。若有任何错误,烦请留言指出。