200字范文,内容丰富有趣,生活中的好帮手!
200字范文 > 实现可能是世上最糟的 React 克隆

实现可能是世上最糟的 React 克隆

时间:2019-11-27 06:47:12

相关推荐

实现可能是世上最糟的 React 克隆

作者:Serge Zaitsev

翻译:New Frontend

德国最近有一个很长的银行假期,空出了不少时间,于是我浮想联翩。我对 React 一直是十动然拒。通常我最终会用一些轻量级的替代品,比如 Preact、superfine、hyperapp、Mithril。我可以浏览它们的源代码,理解各种机制是如何实现的,这种满足感是我选择这些轻量级替代品的原因。另外,提前声明,我不是前端开发者,所以请以批判的眼光阅读这篇文章。

不知怎的,今天早上我产生了这样一个念头,实现一个愚蠢的 React 克隆需要做什么?这会是一个特别缓慢,错漏百出,基本不可用的克隆,不过这也会是一个我亲手实现的克隆。

准备好了吗?

Hyperscript

React 类框架使用 JSX 描述布局。可是 JSX 不过是 JavaScript 的语法扩展,生产环境的代码中可没有 JSX(会被转译为普通的 JavaScript 代码)。许多读者都知道,React 框架内部,JSX 是用许多嵌套的 createElement()调用表示的。每个函数调用声明一个 DOM 节点或组件,包括具体的标签名称,属性集合,子节点列表,这些都以类似的函数调用表示。下面这两段布局代码是等价的:

//使用JSX<divonClick={handleClick}><h1className="header">Hello</h1></div>//使用createElement()createElement("div",{onClick:handleClick},createElement("h1",{className:"header"},"Hello"));

事实上,后面一种语法在 React 出名前就有了,称为 hyperscript。它和 createElement 一模一样,只是使用较短的函数名而已(h(tag, props, ...children))。

我们丑陋的 React 克隆里也有 h()函数,这个函数把参数封装成对象,留待渲染阶段处理:

//我们用的微小的 Hyperscript 函数。//`el`是元素名(标签或组件)//`props`是属性表//`children`是子元素数组consth=(el,props,...children)=>({el,props,children});

现在我们看下如何渲染 hyperscript 布局。

渲染

一般来说,我们需要一个 render(virtNode, domNode)函数将一组虚拟节点渲染为现有的真实 DOM 节点的子元素。

我们常常只需要传递一个虚拟节点,不过有时候也需要传递一组虚拟节点。所以我们使用[].concat()这个小技巧来处理这两种情况(将单个元素转换为数组,将数组扁平化)。

接着遍历每个虚拟节点。在我们简陋的 React 克隆中,节点可能是对象(hyperscript 调用的结果),字符串(DOM 节点间的纯文本),函数(返回 hyperscript 结构的函数组件)。

我们调用函数时会传入相应的属性表,子元素列表,以及特殊的 forceUpdate 函数,调用 forceUpdate 函数会重新渲染整个组件。之后我们给有状态的组件加上动态行为时会用到这个函数。

接下来我们创建一个构建函数,这个函数会根据虚拟节点的类型,创建一个新的 DOM 元素或文本节点。等我们检查完虚拟节点和真实 DOM 元素的差别后才会调用这个构建函数。

如果不存在真实 DOM 元素,或者标签不一样——我们调用构建函数插入新创建的 DOM 元素。

然后我们将所有虚拟节点的属性保存到真实节点。它们将用于下一个渲染周期的虚拟节点和真实节点比较。如果真实节点储存的属性不同,那就重新赋值。

此时 DOM 节点和虚拟节点是一致的,我们在节点子元素上递归调用渲染函数。

最后,所有虚拟节点处理完毕,并复制到真实 DOM 后,我们移除真实 DOM 树上的遗留 DOM。

consth=(el,props,...children)=>({el,props,children});constrender=(vnodes,dom)=>{vnodes=[].concat(vnodes);constforceUpdate=()=>render(vnodes,dom);vnodes.forEach((v,i)=>{while(typeofv.el==="function"){v=v.el(v.props,v.children,forceUpdate);}constnewNode=()=>v.el?document.createElement(v.el):document.createTextNode(v);letnode=dom.childNodes[i];if(!node||(node.el!==v.el&&node.data!==v)){node=dom.insertBefore(newNode(),node);}if(v.el){node.el=v.el;for(letpropNameinv.props){if(node[propName]!==v.props[propName]){node[propName]=v.props[propName];}}render(v.children,node);}else{node.data=v;}});for(letc;(c=dom.childNodes[vnodes.length]);){dom.removeChild(c);}};//ExampleconstHeader=(props,children)=>(h("h1",{style:"color:red"},...children));render(h(Header,{},"Hello","World"),document.body);

上面的代码会渲染出红色的「Hello World」文本。

有状态的组件

正经的 React 克隆会使用键来智能地给 DOM 树打补丁,也会使用键联系在渲染过程中移动了的组件的状态,还会使用 hook(hook 与组件相连,可以用来智能地管理组件状态)。

我决定暂时不在这上面花太多时间,直接给每个组件配上一个 forceUpdate 回调。任何事件监听器都可以调用这个回调函数,强制重新渲染整个组件。不妨想象下末日即将来临,放纵一下,把状态保存在全局变量中。

letn=0;constCounter=(props,children,forceUpdate)=>{consthandleClick=()=>{n++;forceUpdate();};returnx`<div><divclassName="count">Count:${n}</div><buttononclick=${handleClick}>Add</button></div>`;};

让我兴味盎然的是,不用那些无意义的转译,就可以模拟 JSX。

标签模板字面量

你多半熟悉 ES6 的模板字面量(用反引号包起来的字符串)。然而,所有现代的浏览器都支持标签字面量,也就是带有前缀的字符串,这个前缀是一个处理模板字符串的函数。这个函数接受一个字符串数组(数组的每个成员是被占位符分隔开来的字符串)和占位符作为参数:

constx=(strings,...fields)=>{...};x`Hello,${user}!`//strings:["Hello","!"];//fields:[user]

现在我们来动手实现一个微型解析器,解析一种类似 HTML 的语言,根据给定的字符串返回 hyperscript 节点。

我准备支持这样的语法:常规标签,比如<{tagName} attr={value} ...>,以/>结尾的自动闭合标签,以</开头的闭合标签,以及标签中间的纯文本。除了占位符,属性必须加引号。就这些。没有 HTML 注释、空格挤压之类的东西。

考虑解析这样一个语言需要的状态机,只需要 3 个状态:

「文本」,查找<或</。

「开」,在开始标签之内,查找到标签结束为止的属性。

「闭」,在闭合标签之内,查找>。

初始状态是「文本」。占位符可能是标签名、属性值、纯文本。也就是说,如果这些位置上的字符串字面量为空,那我们将使用占位符,否则我们继续读取字符串字面量。

最终的解析器大概是这样的:

exportconstx=(strings,...fields)=>{conststack=[{children:[]}];constfind=(s,re,arg)=>{if(!s){return[s,arg];}letm=s.match(re);return[s.substring(m[0].length),m[1]];};constMODE_TEXT=0;constMODE_OPEN=1;constMODE_CLOSE=2;letmode=MODE_TEXT;strings.forEach((s,i)=>{while(s){letval;s=s.trimLeft();switch(mode){caseMODE_TEXT:if(s[0]==="<"){if(s[1]==="/"){[s,val]=find(s.substring(2),/^([a-zA-Z]+)/,fields[i]);mode=MODE_CLOSE;}else{[s,val]=find(s.substring(1),/^([a-zA-Z]+)/,fields[i]);mode=MODE_OPEN;stack.push(h(val,{},[]));}}else{[s,val]=find(s,/^([^<]+)/,"");stack[stack.length-1].children.push(val);}break;caseMODE_OPEN:if(s[0]==="/"&&s[1]===">"){s=s.substring(2);stack[stack.length-2].children.push(stack.pop());mode=MODE_TEXT;}elseif(s[0]===">"){s=s.substring(1);mode=MODE_TEXT;}else{letm=s.match(/^([a-zA-Z0-9]+)=/);console.assert(m);s=s.substring(m[0].length);letpropName=m[1];[s,val]=find(s,/^"([^"]*)"/,fields[i]);stack[stack.length-1].props[propName]=val;}break;caseMODE_CLOSE:console.assert(s[0]===">");stack[stack.length-2].children.push(stack.pop());s=s.substring(1);mode=MODE_TEXT;break;}}if(mode===MODE_TEXT){stack[stack.length-1].children.push(fields[i]);}});returnstack[0].children[0];};

这个解析器大概极其笨拙缓慢,不过看起来可以工作:

constHello=({onClick},children)=>x`<divclassName="foo"onclick=${onClick}>${children}</div>`;render(h(Hello,{onClick:()=>{}},"Helloworld"),document.body);

更多

现在我们得到了 React 的草率克隆。不过我决定稍微深入一下,把它放到 GitHub 上。在 GitHub 上的代码略微修改了渲染算法,以支持键。上面还有一些测试,让这个玩笑煞有其事起来。我特想支持 hooks,看起来是做到了。

这个库的名字是「O!」听起来既像是在你理解了它是多么简单之后发出的顿悟的叹声,又像是你决定在生产环境使用它碰到严重错误后发出的绝望的吼声。同时,它看起来像是零,这是一个关于它的尺寸大小和有用程度的双重隐喻——我有没有说过,这个包含「JSX」、hook 等艺术的库压缩之后小于 1 KB?

Github 项目在此:/zserge/o 请别指望能有什么实际使用的技术支持,不过我很乐意收到你的反馈(你可以提工单或合并请求)!

不管怎么说,这是一个美妙的早晨。谢谢阅读,愿你我都能从中有所收获!

end

LeanCloud,领先的 BaaS 提供商,为移动开发提供强有力的后端支持。更多内容请关注「LeanCloud 通讯」

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。