[技术地图] 从Preact中了解组件和hooks基本原理

eact 的代码库现在已经比较庞大了,加上 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 其实就是一颗对象树,没有什么特别的,这个对象树最终要映射到图形对象. VirtualDOM比较核心的是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 本身,比如可以渲染为 RN,PDF,终端 UI 等等。

从 createElement 开始
很多小白将 JSX 等价为 Virtual-DOM,其实这两者并没有直接的关系, 我们知道 JSX 不过是一个语法糖.
例如Home最终会转换为h(‘a’, { href:’/’ }, h(‘span’, null, ‘Home’))这种形式, h是 JSX Element 工厂方法,h 在 React 下约定是React.createElement.
h 是 createElement 的别名, Vue 生态系统也是使用这个惯例, 具体为什么没作考究(比较简短?)。
可是使用@jsx注解或 babel 配置项来配置 JSX 工厂:
/**

  • @jsx h
    */
    render(
    hello jsx
    , el);
    复制代码本文不是 react 或 preact 的入门文章,所以点到为止,更多内容可以查看官方教程. 现在来看看createElemet, createElement 不过就是构造一个对象(VNode):

// ?️type 节点的类型,有DOM元素(string)和自定义组件,以及Fragment, 为null时表示文本节点
export function createElement(type, props, children) {
props.children = children;
// ?️应用defaultProps
if (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);
}

export function createVNode(type, props, key, ref) {
return { type, props, key, ref, /* … 忽略部分内置字段 */ constructor: undefined };
}
复制代码

Component 的实现
对于一个视图框架来说,组件就是它的灵魂. 将一个应用分而治之, 拆分和组合不同级别的组件,可以简化应用的开发和维护,让程序更好理解. 组件是一个自定义的元素类型,可以声明组件的输入(props)、有自己的生命周期和状态以及方法、输出 Virtual-DOM 对象树.
自定义组件是基于 Component 类实现的. 对组件来说最基本的就是状态的维护, 这个通过 setState 来实现:

function Component(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 = typeof Promise == ‘function’
? Promise.prototype.then.bind(Promise.resolve()) // micro task
: setTimeout; // 回调到setTimeout

function enqueueRender© {
// 不需要重复推入已经在队列的Component
if (!c._dirty && (c._dirty = true) && q.push© === 1) {
// 当队列从空变为非空时,开始调度
defer(process);
}
}

// 批量清空队列, 调用Component的forceUpdate
function process() {
let p;
// 排序队列,从低层的组件优先更新?
q.sort((a, b) => b._depth - a._depth);
while ((p = q.pop()))
if (p._dirty) p.forceUpdate(false); // false表示不要强制更新,即不要忽略shouldComponentUpdate
}
复制代码

Ok, 上面的代码可以看出 setState 本质上是调用 forceUpdate 进行组件重新渲染的,来看看 forceUpdate 的实现. 这里暂且忽略 diff, 将 diff 视作一个黑盒,他就是一个 DOM 映射器, 像上面说的 diff 接收两棵 VNode 树, 以及一个 DOM 挂载点, 在比对的过程中它可以会创建、移除或更新组件和 DOM 元素,触发对应的生命周期方法.

Component.prototype.forceUpdate = function(callback) { // callback放置渲染完成后的回调
let vnode = this._vnode, dom = this._vnode._dom, parentDom = this._parentDom;

if (parentDom) { // 已挂载过
const force = callback !== false;
let mounts = [];
// 调用diff对当前组件进行重新渲染和Virtual-DOM比对
// ?️暂且忽略这些参数, 将diff视作一个黑盒,他就是一个DOM映射器,
dom = diff(parentDom, vnode, vnode, mounts, this._ancestorComponent, force, dom);
if (dom != null && dom.parentNode !== parentDom)
parentDom.appendChild(dom);
commitRoot(mounts, vnode);
}
if (callback) callback();
};
复制代码

在看看 render 方法, 实现跟 forceUpdate 差不多, 都是调用diff算法来执行DOM更新,只不过由外部指定一个 DOM 容器:

// 简化版
export function render(vnode, parentDom) {
vnode = createElement(Fragment, null, [vnode]);
parentDom.childNodes.forEach(i => i.remove())
let mounts = [];
diffChildren(parentDom, null oldVNode, mounts, vnode, EMPTY_OBJ);
commitRoot(mounts, vnode);
}
复制代码

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值