constApp=(<div><h1className="primary">QndReact is Quick and dirty react</h1><p>It is about building your own React in 90 lines of JavsScript</p></div>);// The above jsx gets converted into/**
* React.createElement(type, attributes, children)
* props: it is the type of the element ie. h1 for <h1></h1>
* attributes: it is an object containing key value pair of props passed to the element
* children: it the array of child elements inside it
*/varApp=React.createElement("div",null,React.createElement("h1",{className:"primary"},"QndReact is Quick and dirty react"),React.createElement("p",null,"It is about building your own React in 90 lines of JavsScript"));
{"plugins":[["@babel/plugin-transform-react-jsx",{"pragma":"QndReact.createElement",// default pragma is React.createElement"throwIfNamespace":false// defaults to true}]]}
// file: src/qnd-react.jsconstcreateElement=(type,props={},...children)=>{console.log(type,props,children);};// to be exported like React.createElementconstQndReact={createElement};exportdefaultQndReact;
// file: src/index.js// QndReact needs to be in scope for JSX to workimportQndReactfrom"./qnd-react";constApp=(<div><h1className="primary">
QndReact is Quick and dirty react
</h1><p>It is about building your own React in 90 lines of JavsScript</p></div>);
现在你应该会在控制台中看到类似这样的内容。
根据以上信息,我们可以使用snabbdom创建自己的内部虚拟 DOM 节点,并将其用于我们的协调流程。首先,我们使用以下命令安装 snabbdom。
npm install snabbdom
现在,每当调用QndReact.createElement(...)时,我们来创建并返回我们的虚拟 DOM 节点。
// file: src/qnd-react.jsimport{h}from'snabbdom';constcreateElement=(type,props={},...children)=>{returnh(type,{props},children);};// to be exported like React.createElementconstQndReact={createElement};exportdefaultQndReact;
现在我们能够解析 JSX 并创建自己的虚拟 DOM 节点了,但是仍然无法将其渲染到浏览器中。为此,我们需要在src/qnd-react-dom.js 文件中添加一个渲染函数。
// file: src/qnd-react-dom.js// React.render(<App />, document.getElementById('root'));// el -> <App />// rootDomElement -> document.getElementById('root')constrender=(el,rootDomElement)=>{// logic to put el into the rootDomElement}// to be exported like ReactDom.renderconstQndReactDom={render};exportdefaultQndReactDom;
与其让我们自己费力地将元素添加到 DOM 中,不如让 snabbdom 来完成这项工作。为此,我们首先需要使用所需的模块初始化 snabbdom。snabbdom 中的模块类似于插件,它们允许 snabbdom 在需要时执行更多操作。
// file: src/qnd-react-dom.jsimport*assnabbdomfrom'snabbdom';importpropsModulefrom'snabbdom/modules/props';// propsModule -> this helps in patching text attributesconstreconcile=snabbdom.init([propsModule]);// React.render(<App />, document.getElementById('root'));// el -> <App />// rootDomElement -> document.getElementById('root')constrender=(el,rootDomElement)=>{// logic to put el into the rootDomElementreconcile(rootDomElement,el);}// to be exported like ReactDom.renderconstQndReactDom={render};exportdefaultQndReactDom;
让我们使用全新的渲染函数在src/index.js中施展一些魔法吧。
// file: src/index.js// QndReact needs to be in scope for JSX to workimportQndReactfrom'./qnd-react';importQndReactDomfrom'./qnd-react-dom';constApp=(<div><h1className="primary">
QndReact is Quick and dirty react
</h1><p>It is about building your own React in 90 lines of JavsScript</p></div>);QndReactDom.render(App,document.getElementById('root'));
瞧!我们应该能在屏幕上看到 JSX 渲染效果了。
等等,我们遇到一个小问题,当我们两次调用 render 函数时,控制台会显示一些奇怪的错误。原因是,我们只能第一次在真正的 DOM 节点上调用reconcile方法,之后我们应该使用第一次调用时返回的虚拟 DOM 节点来调用它。
// file: src/qnd-react-dom.jsimport*assnabbdomfrom'snabbdom';importpropsModulefrom'snabbdom/modules/props';// propsModule -> this helps in patching text attributesconstreconcile=snabbdom.init([propsModule]);// we need to maintain the latest rootVNode returned by renderletrootVNode;// React.render(<App />, document.getElementById('root'));// el -> <App />// rootDomElement -> document.getElementById('root')constrender=(el,rootDomElement)=>{// logic to put el into the rootDomElement// ie. QndReactDom.render(<App />, document.getElementById('root'));// happens when we call render for the first timeif(rootVNode==null){rootVNode=rootDomElement;}// remember the VNode that reconcile returnsrootVNode=reconcile(rootVNode,el);}// to be exported like ReactDom.renderconstQndReactDom={render};exportdefaultQndReactDom;
// file: src/index.js// QndReact needs to be in scope for JSX to workimportQndReactfrom"./qnd-react";importQndReactDomfrom"./qnd-react-dom";// functional component to welcome someoneconstGreeting=({name})=><p>Welcome {name}!</p>;constApp=(<div><h1className="primary">
QndReact is Quick and dirty react
</h1><p>It is about building your own React in 90 lines of JavsScript</p><Greetingname={"Ameer Jhan"}/></div>);QndReactDom.render(App,document.getElementById("root"));
我们可以看到,当组件是函数式组件时,传递的参数类型都是 JavaScript函数。调用该函数后,我们将得到组件想要渲染的 HTML 结果。
现在我们需要检查类型参数的类型是否为函数,如果是,则调用该函数作为type(props),如果不是,则将其作为普通的 HTML 元素处理。
// file: src/qnd-react.jsimport{h}from'snabbdom';constcreateElement=(type,props={},...children)=>{// if type is a function then call it and return it's valueif (typeof (type)=='function'){returntype(props);}returnh(type,{props},children);};// to be exported like React.createElementconstQndReact={createElement};exportdefaultQndReact;
// file: src/qnd-react.jsimport{h}from"snabbdom";constcreateElement=(type,props={},...children)=>{// if type is a function then call it and return it's valueif (typeoftype=="function"){returntype(props);}returnh(type,{props},children);};// component base classclassComponent{constructor(){}componentDidMount(){}setState(partialState){}render(){}}// to be exported like React.createElement, React.ComponentconstQndReact={createElement,Component};exportdefaultQndReact;
// file: src/index.js// QndReact needs to be in scope for JSX to workimportQndReactfrom"./qnd-react";importQndReactDomfrom"./qnd-react-dom";importCounterfrom"./counter";// functional component to welcome someoneconstGreeting=({name})=><p>Welcome {name}!</p>;constApp=(<div><h1className="primary">
QndReact is Quick and dirty react
</h1><p>It is about building your own React in 90 lines of JavsScript</p><Greetingname={"Ameer Jhan"}/><Counter/></div>);QndReactDom.render(App,document.getElementById("root"));
// file: src/qnd-react.jsimport{h}from"snabbdom";constcreateElement=(type,props={},...children)=>{console.log(typeof (type),type);// if type is a function then call it and return it's valueif (typeoftype=="function"){returntype(props);}returnh(type,{props},children);};...
现在查看控制台,看看记录了什么。
你可以看到 Counter 的类型也是一个函数,这是因为Babel最终会将 ES6 类转换为普通的 JavaScript 函数,那么我们该如何处理 Class 组件的情况呢?我们可以给组件基类添加一个静态属性,用来检查传递的类型参数是否为 Class。React 也是这样处理的,你可以阅读 Dan 的博客了解更多信息(链接在此) 。
// file: src/qnd-react.jsimport{h}from"snabbdom";...// component base classclassComponent{constructor(){}componentDidMount(){}setState(partialState){}render(){}}// add a static property to differentiate between a class and a functionComponent.prototype.isQndReactClassComponent=true;// to be exported like React.createElement, React.ComponentconstQndReact={createElement,Component};exportdefaultQndReact;
现在让我们添加一些代码来处理QndReact.createElement(...)中的 Class 组件。
// file: src/qnd-react.jsimport{h}from"snabbdom";constcreateElement=(type,props={},...children)=>{// if type is a Class then// 1. create a instance of the Class// 2. call the render method on the Class instanceif (type.prototype&&type.prototype.isQndReactClassComponent){constcomponentInstance=newtype(props);returncomponentInstance.render();}// if type is a function then call it and return it's valueif (typeoftype=="function"){returntype(props);}returnh(type,{props},children);};// component base classclassComponent{constructor(){}componentDidMount(){}setState(partialState){}render(){}}// add a static property to differentiate between a class and a functionComponent.prototype.isQndReactClassComponent=true;// to be exported like React.createElement, React.ComponentconstQndReact={createElement,Component};exportdefaultQndReact;
// file: src/qnd-react-dom.jsimportQndReactfrom'./qnd-react';import*assnabbdomfrom'snabbdom';importpropsModulefrom'snabbdom/modules/props';...// QndReactDom telling React how to update DOMQndReact.__updater=()=>{// logic on how to update the DOM when you call this.setState}// to be exported like ReactDom.renderconstQndReactDom={render};exportdefaultQndReactDom;
// file: src/qnd-react.jsimport{h}from"snabbdom";constcreateElement=(type,props={},...children)=>{// if type is a Class then// 1. create a instance of the Class// 2. call the render method on the Class instanceif (type.prototype&&type.prototype.isQndReactClassComponent){constcomponentInstance=newtype(props);// remember the current vNode instancecomponentInstance.__vNode=componentInstance.render();returncomponentInstance.__vNode;}// if type is a function then call it and return it's valueif (typeoftype=="function"){returntype(props);}returnh(type,{props},children);};// component base classclassComponent{constructor(){}componentDidMount(){}setState(partialState){}render(){}}// add a static property to differentiate between a class and a functionComponent.prototype.isQndReactClassComponent=true;// to be exported like React.createElement, React.ComponentconstQndReact={createElement,Component};exportdefaultQndReact;
现在让我们在Component基类中实现setState函数。
// file: src/qnd-react.jsimport{h}from"snabbdom";...// component base classclassComponent{constructor(){}componentDidMount(){}setState(partialState){// update the state by adding the partial statethis.state={...this.state,...partialState}// call the __updater function that QndReactDom gaveQndReact.__updater(this);}render(){}}// add a static property to differentiate between a class and a functionComponent.prototype.isQndReactClassComponent=true;// to be exported like React.createElement, React.ComponentconstQndReact={createElement,Component};exportdefaultQndReact;
好了,现在让我们来处理QndReactDom中的__updater函数。
// file: src/qnd-react-dom.jsimportQndReactfrom'./qnd-react';import*assnabbdomfrom'snabbdom';importpropsModulefrom'snabbdom/modules/props';...// QndReactDom telling React how to update DOMQndReact.__updater=(componentInstance)=>{// logic on how to update the DOM when you call this.setState// get the oldVNode stored in __vNodeconstoldVNode=componentInstance.__vNode;// find the updated DOM node by calling the render methodconstnewVNode=componentInstance.render();// update the __vNode property with updated __vNodecomponentInstance.__vNode=reconcile(oldVNode,newVNode);}...exportdefaultQndReactDom;
太棒了!现在让我们通过向Counter 组件添加状态来检查setState实现是否有效。
importQndReactfrom'./qnd-react';exportdefaultclassCounterextendsQndReact.Component{constructor(props){super(props);this.state={count:0}// update the count every secondsetInterval(()=>{this.setState({count:this.state.count+1})},1000);}componentDidMount(){console.log('Component mounted');}render(){return<p>Count: {this.state.count}</p>}}
太好了,我们的计数器组件已经按预期工作了。
让我们添加ComponentDidMount生命周期钩子。Snabbdom 提供了一些钩子,通过这些钩子我们可以知道虚拟 DOM 节点是否在实际 DOM 中被添加、销毁或更新,您可以在这里了解更多信息。
// file: src/qnd-react.jsimport{h}from"snabbdom";constcreateElement=(type,props={},...children)=>{// if type is a Class then// 1. create a instance of the Class// 2. call the render method on the Class instanceif (type.prototype&&type.prototype.isQndReactClassComponent){constcomponentInstance=newtype(props);// remember the current vNode instancecomponentInstance.__vNode=componentInstance.render();// add hook to snabbdom virtual node to know whether it was added to the actual DOMcomponentInstance.__vNode.data.hook={create:()=>{componentInstance.componentDidMount()}}returncomponentInstance.__vNode;}// if type is a function then call it and return it's valueif (typeoftype=="function"){returntype(props);}returnh(type,{props},children);};...exportdefaultQndReact;
太好了,我们已经完成了支持 componentDidMount 生命周期钩子的 Class 组件的实现。
// file: src/qnd-react-dom.jsimport*assnabbdomfrom'snabbdom';importpropsModulefrom'snabbdom/modules/props';importeventlistenersModulefrom'snabbdom/modules/eventlisteners';importQndReactfrom'./qnd-react';// propsModule -> this helps in patching text attributes// eventlistenersModule -> this helps in patching event attributesconstreconcile=snabbdom.init([propsModule,eventlistenersModule]);...
Snabdom 要求将文本属性和事件属性作为两个独立的对象,所以我们就这样做。
// file: src/qnd-react.jsimport{h}from'snabbdom';constcreateElement=(type,props={},...children)=>{...props=props||{};letdataProps={};leteventProps={};// This is to seperate out the text attributes and event listener attributesfor(letpropKeyinprops){// event props always startwith on eg. onClick, onDblClick etc.if (propKey.startsWith('on')){// onClick -> clickconstevent=propKey.substring(2).toLowerCase();eventProps[event]=props[propKey];}else{dataProps[propKey]=props[propKey];}}// props -> snabbdom's internal text attributes// on -> snabbdom's internal event listeners attributesreturnh(type,{props:dataProps,on:eventProps},children);};...// to be exported like React.createElement, React.ComponentconstQndReact={createElement,Component};exportdefaultQndReact;