作者:荒山
https://juejin.im/post/5cfa29e151882539c33e4f5e
React 的代码库现在已经比较庞大了,加上 v16 的 Fiber 重构,初学者很容易陷入细节的汪洋大海,搞懂了会让人觉得自己很牛逼,搞不懂很容易让人失去信心, 怀疑自己是否应该继续搞前端。那么尝试在本文这里找回一点自信吧(高手绕路).
Preact 是 React 的缩略版, 体积非常小, 但五脏俱全. 如果你想了解 React 的基本原理, 可以去学习学习 Preact 的源码, 这也正是本文的目的。
关于 React 原理的优秀的文章已经非常多, 本文就是老酒装新瓶, 算是自己的一点总结,也为后面的文章作一下铺垫吧.
文章篇幅较长,阅读时间约 20min,主要被代码占据,另外也画了流程图配合理解代码。
注意:代码有所简化,忽略掉 svg、replaceNode、context 等特性 本文代码基于 Preact v10 版本
-
Virtual-DOM
-
从 createElement 开始
-
Component 的实现
-
diff 算法
-
diffChildren
-
diff
-
diffElementNodes
-
diffProps
-
-
Hooks 的实现
-
useState
-
useEffect
-
-
技术地图
-
扩展
Virtual-DOM
Virtual-DOM 其实就是一颗对象树,没有什么特别的,这个对象树最终要映射到图形对象. Virtual-DOM 比较核心的是它的diff算法
.
你可以想象这里有一个DOM映射器
,见名知义,这个’DOM 映射器‘的工作就是将 Virtual-DOM 对象树映射浏览器页面的 DOM,只不过为了提高 DOM 的'操作性能'. 它不是每一次都全量渲染整个 Virtual-DOM 树,而是支持接收两颗 Virtual-DOM 对象树(一个更新前,一个更新后), 通过 diff 算法计算出两颗 Virtual-DOM 树差异的地方,然后只应用这些差异的地方到实际的 DOM 树, 从而减少 DOM 变更的成本.
Virtual-DOM 是比较有争议性,推荐阅读《网上都说操作真实 DOM 慢,但测试结果却比 React 更快,为什么?》 。切记永远都不要离开场景去评判一个技术的好坏。当初网上把 React 吹得多么牛逼, 一些小白就会觉得 Virtual-DOM 很吊,JQuery 弱爆了。
我觉得两个可比性不大,从性能上看, 框架再怎么牛逼它也是需要操作原生 DOM 的,而且它未必有你使用 JQuery 手动操作 DOM 来得'精细'. 框架不合理使用也可能出现修改一个小状态,导致渲染雪崩(大范围重新渲染)的情况; 同理 JQuery 虽然可以精细化操作 DOM, 但是不合理的 DOM 更新策略可能也会成为应用的性能瓶颈. 所以关键还得看你怎么用.
那为什么需要 Virtual-DOM?
我个人的理解就是为了解放生产力。现如今硬件的性能越来越好,web 应用也越来越复杂,生产力也是要跟上的. 尽管手动操作 DOM 可能可以达到更高的性能和灵活性,但是这样对大部分开发者来说太低效了,我们是可以接受牺牲一点性能换取更高的开发效率的.
所以说 Virtual-DOM 更大的意义在于开发方式的改变: 声明式、 数据驱动, 让开发者不需要关心 DOM 的操作细节(属性操作、事件绑定、DOM 节点变更),也就是说应用的开发方式变成了view=f(state)
, 这对生产力的解放是有很大推动作用的.
当然 Virtual-DOM 不是唯一,也不是第一个的这样解决方案. 比如 AngularJS, Vue1.x 这些基于模板的实现方式, 也可以说实现这种开发方式转变的. 那相对于他们 Virtual-DOM 的买点可能就是更高的性能了, 另外 Virtual-DOM 在渲染层上面的抽象更加彻底, 不再耦合于 DOM 本身,比如可以渲染为 ReactNative,PDF,终端 UI 等等。
从 createElement 开始
很多小白将 JSX
等价为 Virtual-DOM,其实这两者并没有直接的关系, 我们知道 JSX 不过是一个语法糖.
例如<a href="/"><span>Home</span></a>
最终会转换为h('a', { href:'/' }, h('span', null, 'Home'))
这种形式, h
是 JSX Element 工厂方法.
h
在 React 下约定是React.createElement
, 而大部分 Virtual-DOM 框架则使用h
. h
是 createElement
的别名, Vue 生态系统也是使用这个惯例, 具体为什么没作考究(比较简短?)。
可以使用@jsx
注解或 babel 配置项来配置 JSX 工厂:
/**
* @jsx h
*/
render(<div>hello jsx</div>, el);
本文不是 React 或 Preact 的入门文章,所以点到为止,更多内容可以查看官方教程.
现在来看看createElement
, createElement 不过就是构造一个对象(VNode):
// ⚛️type 节点的类型,有DOM元素(string)和自定义组件,以及Fragment, 为null时表示文本节点exportfunctioncreateElement(type, props, children) {
props.children = children;
// ⚛️应用defaultPropsif (type != null && type.defaultProps != null)
for (let i in type.defaultProps)
if (props[i] === undefined) props[i] = type.defaultProps[i];
let ref = props.ref;
let key = props.key;
// ...// ⚛️构建VNode对象return createVNode(type, props, key, ref);
}
exportfunctioncreateVNode(type, props, key, ref) {
return { type, props, key, ref, /* ... 忽略部分内置字段 */constructor: undefined };
}
通过 JSX 和组件, 可以构造复杂的对象树:
render(
<divclassName="container"><SideBar /><Body /></div>,
root,
);
Component 的实现
对于一个视图框架来说,组件就是它的灵魂, 就像函数之于函数式语言,类之于面向对象语言, 没有组件则无法组成复杂的应用.
组件化的思维推荐将一个应用分而治之, 拆分和组合不同级别的组件,这样可以简化应用的开发和维护,让程序更好理解. 从技术上看组件是一个自定义的元素类型,可以声明组件的输入(props)、有自己的生命周期和状态以及方法、最终输出 Virtual-DOM 对象树, 作为应用 Virtual-DOM 树的一个分支存在.
Preact 的自定义组件是基于 Component 类实现的. 对组件来说最基本的就是状态的维护, 这个通过 setState 来实现:
functionComponent(props, context) {}
// ⚛️setState实现
Component.prototype.setState = function(update, callback) {
// 克隆下一次渲染的State, _nextState会在一些生命周期方式中用到(例如shouldComponentUpdate)let s = (this._nextState !== this.state && this._nextState) ||
(this._nextState = assign({}, this.state));
// state更新if (typeof update !== 'function' || (update = update(s, this.props)))
assign(s, update);
if (this._vnode) { // 已挂载// 推入渲染回调队列, 在渲染完成后批量调用if (callback) this._renderCallbacks.push(callback);
// 放入异步调度队列
enqueueRender(this);
}
};
enqueueRender
将组件放进一个异步的批执行队列中,这样可以归并频繁的 setState 调用,实现也非常简单:
let q = [];
// 异步调度器,用于异步执行一个回调const defer = typeofPromise == 'function'
? Promise.prototype.then.bind(Promise.resolve()) // micro task
: setTimeout; // 回调到setTimeoutfunctionenqueueRender(c) {
// 不需要重复推入已经在队列的Componentif (!c._dirty && (c._dirty = true) && q.push(c) === 1)
defer(process); // 当队列从空变为非空时,开始调度
}