Open li-jia-nan opened 1 year ago
这篇文章带大家实现一个简单的render函数,在此之前,你需要对jsx语法和DOM元素的工作原理有基本了解
const element = <h1 title="foo">Hello</h1>; const container = document.getElementById("root"); ReactDOM.render(element, container);
我们将实现这个React渲染函数,只有三行代码:
注意知道的是:在这里我们只实现函数本身,不会关心jsx是如何编译成render函数的,因为那是编译器(比如Babel)的工作,jsx在编译的时候,通过某些构建工具(比如Babel)转换为js,转换过程很简单:用createElement函数代替我们定义的内容,同时将标签名、props、子元素作为参数传递给createElement函数
让我们删除所有React的代码,用普通的JavaScript代替它:
在上面的代码中,第一行是用jsx定义的元素,实际上它并不是有效的JavaScript,所以为了使用有效的js,首先我们需要用createElement替换掉jsx:
const element = React.createElement( "h1", { title: "foo" }, "Hello" );
然后再把上面代码中的createElement函数转换成普通的element对象,可以写成下面这样:
const element = { type: "h1", props: { title: "foo", children: "Hello", }, };
可以看到,createElement函数的作用是根据其参数创建一个对象,对象有两个属性:type和props
我们需要替换的另一部分React代码是对ReactDOM.render的调用:
const element = { type: "h1", props: { title: "foo", children: "Hello", }, }; const container = document.getElementById("root"); // 获取根节点 const node = document.createElement(element.type); // 使用type创建一个节点 node["title"] = element.props.title; // 将title属性赋值给该节点 const text = document.createTextNode(""); // 创建子节点 text["nodeValue"] = element.props.children; // 用nodeValue设置子节点的内容 node.appendChild(text); // 将子节点append到父节点 container.appendChild(node); // 将父节点append到根节点
现在,我们有了和开始几行代码一样的功能,但是没有使用React
接下来,让我们开始编写自己的createElement函数:
const element = React.createElement( "div", { id: "foo" }, React.createElement("a", null, "bar"), React.createElement("b"), );
正如我们在前面的步骤中所看到的:element是一个具有type和props的对象。我们的函数唯一需要做的就是创建这个对象:
function createElement(type, props, ...children) { return { type, props: { ...props, children, }, }; }
我们对props使用扩展操作符,对children参数使用rest语法,这样可以保证子元素props将始终是一个数组,例如:
createElement("div")
{ "type": "div", "props": { "children": [] }, }
createElement("div", null, a)
{ "type": "div", "props": { "children": [a] }, }
createElement("div", null, a, b)
{ "type": "div", "props": { "children": [a, b] }, }
需要注意的是:children还可以包含string或者number等基本类型的值。因此,我们需要先写一个函数区分基本类型的值和对象类型的值,然后把所有不是对象类型的值封装一下,并为它们创建一个特殊的类型:TEXT_ELEMENT
string
number
基本类型
TEXT_ELEMENT
function createTextElement(text) { return { type: "TEXT_ELEMENT", props: { nodeValue: text, children: [], }, }; } function createElement(type, props, ...children) { return { type, props: { ...props, children: children.map(child => ( typeof child === "object" ? child : createTextElement(child) )), }, } }
需要知道的是,在react源码中,当没有子元素时,React不会封装原始值、也不会创建空数组,而我们这样做的原因,仅仅是因为我太懒,这样写起来简单,并且对于我们简易版的render函数,我更喜欢简单的代码、而不是性能代码。所以不用去追求细节(躺平就完了)
然后仍然回到React的createElement函数:
为了替换它,我们给自己的函数起个新的名字:myReactRender
const myReactRender = { createElement, }; const element = myReactRender.createElement( "div", { id: "foo" }, myReactRender.createElement("a", null, "bar"), myReactRender.createElement("b"), );
目前已经实现了createElement函数,接下来需要实现render函数:
function render(element, container) { // TODO create dom nodes } const myReactRender = { createElement, render, }; const element = myReactRender.createElement( "div", { id: "foo" }, myReactRender.createElement("a", null, "bar"), myReactRender.createElement("b"), ); const container = document.getElementById("root"); myReactRender.render(element, container);
接下来需要处理render函数:
function render(element, container) { const dom = element.type == "TEXT_ELEMENT" ? document.createTextNode("") : document.createElement(element.type); element.props.children.forEach(child => render(child, dom)); container.appendChild(dom); }
最后一件事,就是将props的每一个值分配给创建的dom:
function render(element, container) { const dom = element.type == "TEXT_ELEMENT" ? document.createTextNode("") : document.createElement(element.type); Object.keys(element.props) .filter(key => key !== "children") .forEach(name => { dom[name] = element.props[name] }); element.props.children.forEach(child => render(child, dom)); container.appendChild(dom); }
最后一步,如果我们仍然想在这里使用jsx语法,我们如何告诉Babel使用myReactRender的渲染函数而不是React的? 我们只需要添加这样的注释就好了,当Babel编译jsx时,它将使用我们定义的函数:
/** @jsx myReactRender.createElement */ const element = ( <div id="foo"> <a>11111</a> </div> );
就是这样,现在我们有了一个可以将JSX渲染到DOM的库,完整代码如下:
function createTextElement(text) { return { type: "TEXT_ELEMENT", props: { nodeValue: text, children: [], }, }; } function createElement(type, props, ...children) { return { type, props: { ...props, children: children.map(child => ( typeof child === "object" ? child : createTextElement(child) )), }, } } function render(element, container) { const dom = element.type == "TEXT_ELEMENT" ? document.createTextNode("") : document.createElement(element.type); Object.keys(element.props) .filter(key => key !== "children") .forEach(name => { dom[name] = element.props[name] }); element.props.children.forEach(child => render(child, dom)); container.appendChild(dom); } const myReactRender = { createElement, render }; const container = document.getElementById("root"); /** @jsx myReactRender.createElement */ const element = ( <div id="foo"> <a>abc</a> </div> ); myReactRender.render(element, container);
截止目前,我们用40行代码实现了react的render函数。
这篇文章带大家实现一个简单的render函数,在此之前,你需要对jsx语法和DOM元素的工作原理有基本了解
我们将实现这个React渲染函数,只有三行代码:
注意知道的是:在这里我们只实现函数本身,不会关心jsx是如何编译成render函数的,因为那是编译器(比如Babel)的工作,jsx在编译的时候,通过某些构建工具(比如Babel)转换为js,转换过程很简单:用createElement函数代替我们定义的内容,同时将标签名、props、子元素作为参数传递给createElement函数
让我们删除所有React的代码,用普通的JavaScript代替它:
在上面的代码中,第一行是用jsx定义的元素,实际上它并不是有效的JavaScript,所以为了使用有效的js,首先我们需要用createElement替换掉jsx:
然后再把上面代码中的createElement函数转换成普通的element对象,可以写成下面这样:
可以看到,createElement函数的作用是根据其参数创建一个对象,对象有两个属性:type和props
我们需要替换的另一部分React代码是对ReactDOM.render的调用:
现在,我们有了和开始几行代码一样的功能,但是没有使用React
接下来,让我们开始编写自己的createElement函数:
正如我们在前面的步骤中所看到的:element是一个具有type和props的对象。我们的函数唯一需要做的就是创建这个对象:
我们对props使用扩展操作符,对children参数使用rest语法,这样可以保证子元素props将始终是一个数组,例如:
createElement("div")
返回:createElement("div", null, a)
返回:createElement("div", null, a, b)
返回:需要注意的是:children还可以包含
string
或者number
等基本类型
的值。因此,我们需要先写一个函数区分基本类型的值和对象类型的值,然后把所有不是对象类型的值封装一下,并为它们创建一个特殊的类型:TEXT_ELEMENT
需要知道的是,在react源码中,当没有子元素时,React不会封装原始值、也不会创建空数组,而我们这样做的原因,仅仅是因为我太懒,这样写起来简单,并且对于我们简易版的render函数,我更喜欢简单的代码、而不是性能代码。所以不用去追求细节(躺平就完了)
然后仍然回到React的createElement函数:
为了替换它,我们给自己的函数起个新的名字:myReactRender
目前已经实现了createElement函数,接下来需要实现render函数:
接下来需要处理render函数:
TEXT_ELEMENT
,我们将创建一个文本节点最后一件事,就是将props的每一个值分配给创建的dom:
最后一步,如果我们仍然想在这里使用jsx语法,我们如何告诉Babel使用myReactRender的渲染函数而不是React的? 我们只需要添加这样的注释就好了,当Babel编译jsx时,它将使用我们定义的函数:
就是这样,现在我们有了一个可以将JSX渲染到DOM的库,完整代码如下:
截止目前,我们用40行代码实现了react的render函数。