现在网上有很多 react 原理解析这样的文章, 但是往往这样的文章我看完过后却没有什么收获, 因为行文思路太快, 大部分就是写了几句话简单介绍下这段代码是用来干嘛的, 然后就贴上源码让你自己看, 有可能作者本人是真的看懂了, 但是对于大部分阅读这篇文章的人来说, 确是云里雾里.
讲解一个框架的源码, 最好的方式就是实现一个简易版的, 这样在你实现的过程中, 读者就能了解到你整体的思路, 也就能站在更高的层面上对框架有一个整体的认知, 而不是陷在一些具体的技术细节上.
这篇文章就非常棒的实现了一个简单的 react 框架, 接下来属于对原文 https://medium.com/@sweetpalma/gooact-react-in-160-lines-of-javascript-44e0742ad60f 的翻译加上一些自己在使用过程中的理解.
首先先整体介绍通过这篇文章你能学到什么 -- 我们将实现一个简单的 React, 包括简单的组件级 api 和虚拟 dom, 文章也将分为以下四个部分
Elements: 在这一章我们将学习 JSX 是如何被处理成虚拟 DOM 的
Rendering: 在这一小节我们将想你展示如何将虚拟 dom 变成真实的 DOM 的
Patching: 在这一章我们将向你展示为什么 key 如此重要, 并且如何利用虚拟 DOM 对已存在的 DOM 进行批量更新
Components : 最后一小节将告诉你 React 组件和他的生命周期
Element
元素携带者很多重要的信息, 比如节点的 type,props,children list, 根据这些属性, 能渲染出我们需要的元素, 它的树形结构如下
- {
- "type": "ul",
- "props": {"className": "some-list"},
- "children": [
- {
- "type": "li",
- "props": {
- "className": "some-list__item"
- },
- "children": [
- "One"
- ]
- },
- {
- "type": "li",
- "props": {
- "className": "some-list__item"
- },
- "children": [
- "Two"
- ]
- }
- ]
- }
但是如果我们日常写代码如果要写成这个样子, 那我们应该要疯了, 所以一般我们会写 jsx 的语法
- /** @jsx createElement */
- const list = <ul className="some-list">
- <li className="some-list__item">One</li>
- <li className="some-list__item">Two</li>
- </ul>;
为了能够让他被编译成常规的方法, 我们需要加上注释来定义用哪个函数, 最终定义的函数被执行, 最后会返回给一个虚拟 DOM
- const createElement = (type, props, ...children) => {
- props = props != null ? props : {};
- return {type, props, children};
- };
我为什么这个地方要加注释呢, 因为我在用 babel 打包 jsx 的语法的时候, 貌似默认用的 React 里提供的 CreateElement, 所以当时我配置了. babelrc 以后
发现它报了一个 React is not defined 错误, 但是我安装的是作者这个简易的类 React 包, 后来才知道在 jsx 前要加一段注释来告诉 babel 编译的时候用哪个函数
- /** @jsx Gooact.createElement */
- Rendering
这一节是将 vdom 渲染真实 dom
上一节我们已经得到了根据 jsx 语法得出的虚拟 dom 树形结构, 那么就该将这个虚拟 dom 结构渲染成真实 dom
那么我们在拿到一个树形结构的时候, 如何判断这个节点应该渲染成真实 dom 的什么样子呢, 这里就会有 3 种情况, 第一种就是直接会返回一个字符串, 那我们就直接生成一个文本节点, 如果返回的是一个我们自定义的组件, 那么我们就在调用这个方法, 如果是一个常规的 dom 组件, 我们就创建这样的一个 dom 元素, 然后接着继续遍历它的子节点.
setAttribute 就是将我们设置在虚拟 dom 上的属性设置在真实 dom 上
- const render = (vdom, parent=null) => {
- if (parent) parent.textContent = '';
- const mount = parent ? (el => parent.appendChild(el)) : (el => el);
- if (typeof vdom == 'string' || typeof vdom == 'number') {
- return mount(document.createTextNode(vdom));
- } else if (typeof vdom == 'boolean' || vdom === null) {
- return mount(document.createTextNode(''));
- } else if (typeof vdom == 'object' && typeof vdom.type == 'function') {
- return mount(Component.render(vdom));
- } else if (typeof vdom == 'object' && typeof vdom.type == 'string') {
- const dom = document.createElement(vdom.type);
- for (const child of [].concat(...vdom.children)) // flatten
- dom.appendChild(render(child));
- for (const prop in vdom.props)
- setAttribute(dom, prop, vdom.props[prop]);
- return mount(dom);
- } else {
- throw new Error(`Invalid VDOM: ${vdom}.`);
- }
- };
- const setAttribute = (dom, key, value) => {
- if (typeof value == 'function' && key.startsWith('on')) {
- const eventType = key.slice(2).toLowerCase();
- dom.__gooactHandlers = dom.__gooactHandlers || {};
- dom.removeEventListener(eventType, dom.__gooactHandlers[eventType]);
- dom.__gooactHandlers[eventType] = value;
- dom.addEventListener(eventType, dom.__gooactHandlers[eventType]);
- } else if (key == 'checked' || key == 'value' || key == 'id') {
- dom[key] = value;
- } else if (key == 'key') {
- dom.__gooactKey = value;
- } else if (typeof value != 'object' && typeof value != 'function') {
- dom.setAttribute(key, value);
- }
- };
- Patching
想象一个你有一个很深的结构, 而且你还需要频繁的更新你的虚拟 dom, 如果你改变了一些, 那么全部都要渲染, 这无疑会消耗很多时间.
但是如果我们有一个算法能够比较出新的虚拟 dom 和已有 dom 的差异, 然后只更新那些改变的地方, 这个地方就是经常说的 React 团队做了一些经过实践后的约定, 将本来 o(n)^3 的时间复杂度降低到了 o(n), 主要就是下面两种主要的约定
两个元素如果有不同的类型那么就会产生两种不同的树
当我们给了一个 key 属性后, 他就会根据它去判断
- const patch = (dom, vdom, parent=dom.parentNode) => {
- const replace = parent ? el => (parent.replaceChild(el, dom) && el) : (el => el);
- if (typeof vdom == 'object' && typeof vdom.type == 'function') {
- return Component.patch(dom, vdom, parent);
- } else if (typeof vdom != 'object' && dom instanceof Text) {
- return dom.textContent != vdom ? replace(render(vdom)) : dom;
- } else if (typeof vdom == 'object' && dom instanceof Text) {
- return replace(render(vdom));
- } else if (typeof vdom == 'object' && dom.nodeName != vdom.type.toUpperCase()) {
- return replace(render(vdom));
- } else if (typeof vdom == 'object' && dom.nodeName == vdom.type.toUpperCase()) {
- const pool = {};
- const active = document.activeElement;
- for (const index in Array.from(dom.childNodes)) {
- const child = dom.childNodes[index];
- const key = child.__gooactKey || index;
- pool[key] = child;
- }
- const vchildren = [].concat(...vdom.children); // flatten
- for (const index in vchildren) {
- const child = vchildren[index];
- const key = child.props && child.props.key || index;
- dom.appendChild(pool[key] ? patch(pool[key], child) : render(child));
- delete pool[key];
- }
- for (const key in pool) {
- if (pool[key].__gooactInstance)
- pool[key].__gooactInstance.componentWillUnmount();
- pool[key].remove();
- }
- for (const attr of dom.attributes) dom.removeAttribute(attr.name);
- for (const prop in vdom.props) setAttribute(dom, prop, vdom.props[prop]);
- active.focus();
- return dom;
- }
- };
- Component
组件是最像 js 中函数的概念了, 我们通过它能够展示出什么应该展示在屏幕上, 它可以被定义成一个无状态的函数, 或者是一个有生命周期的组件.
- class Component {
- constructor(props) {
- this.props = props || {};
- this.state = null;
- }
- static render(vdom, parent=null) {
- const props = Object.assign({}, vdom.props, {children: vdom.children});
- if (Component.isPrototypeOf(vdom.type)) {
- const instance = new (vdom.type)(props);
- instance.componentWillMount();
- instance.base = render(instance.render(), parent);
- instance.base.__gooactInstance = instance;
- instance.base.__gooactKey = vdom.props.key;
- instance.componentDidMount();
- return instance.base;
- } else {
- return render(vdom.type(props), parent);
- }
- }
- static patch(dom, vdom, parent=dom.parentNode) {
- const props = Object.assign({}, vdom.props, {children: vdom.children});
- if (dom.__gooactInstance && dom.__gooactInstance.constructor == vdom.type) {
- dom.__gooactInstance.componentWillReceiveProps(props);
- dom.__gooactInstance.props = props;
- return patch(dom, dom.__gooactInstance.render());
- } else if (Component.isPrototypeOf(vdom.type)) {
- const ndom = Component.render(vdom);
- return parent ? (parent.replaceChild(ndom, dom) && ndom) : (ndom);
- } else if (!Component.isPrototypeOf(vdom.type)) {
- return patch(dom, vdom.type(props));
- }
- }
- setState(nextState) {
- if (this.base && this.shouldComponentUpdate(this.props, nextState)) {
- const prevState = this.state;
- this.componentWillUpdate(this.props, nextState);
- this.state = nextState;
- patch(this.base, this.render());
- this.componentDidUpdate(this.props, prevState);
- } else {
- this.state = nextState;
- }
- }
- shouldComponentUpdate(nextProps, nextState) {
- return nextProps != this.props || nextState != this.state;
- }
- componentWillReceiveProps(nextProps) {
- return undefined;
- }
- componentWillUpdate(nextProps, nextState) {
- return undefined;
- }
- componentDidUpdate(prevProps, prevState) {
- return undefined;
- }
- componentWillMount() {
- return undefined;
- }
- componentDidMount() {
- return undefined;
- }
- componentWillUnmount() {
- return undefined;
- }
- }
本次文章中新开发的 gooact 轮子就结束了, 让我们看看他有什么功能
它能够高效的更新复杂的 dom 结构
支持函数式和状态式两种组件
那它距离一个完整的 React 应用还差什么呢?
他还不支持 fragments,portals 这样的新版本的特性
因为 React Fiber 太复杂了, 目前还没有支持
如果你写了重复的 key, 可能会有 bug
对于一些方法, 还少了一些回调函数
但是这篇文章是不是给你带来一个全新的视角看 React 框架, 让你对这个框架做的事情有了一个全局的了解呢?
反正笔者看了原文对 React 框架思路又更加清晰了, 最后献上使用这个框架的用例 demo https://github.com/z2014/Blog/tree/master/simple-react
来源: https://juejin.im/post/5b0a697f518825389c508872