从零开始实现一个React(二):实现组件功能
前言在上一篇文章JSX和虚拟DOM中,我们实现了基础的JSX渲染功能,但是React的意义在于组件化。在这篇文章中,我们就要实现React的组件功能。 React定义组件的方式可以分为两种:函数和类,我们姑且将两种不同方式定义的组件称之为函数定义组件和类定义组件 函数定义组件函数定义组件相对简单,只需要用组件名称声明一个函数,并返回一段JSX即可。 function Welcome( props ) { return <h1>Hello,{props.name}</h1>; } 注意组件名称要以大写字母开头 函数组件接受一个props参数,它是给组件传入的数据。 我们可以这样来使用它: const element = <Welcome name="Sara" />; ReactDOM.render( element,document.getElementById( 'root' ) ); 让createElemen支持函数定义组件回顾一下上一篇文章中我们对 function createElement( tag,attrs,...children ) { return { tag,children } } 这种实现只能渲染原生DOM元素,而对于组件,createElement得到的参数略有不同:
区分组件和原生DOM的工作,是
例如在处理 function Welcome( props ) { return <h1>Hello,{props.name}</h1>; } 所以我们需要修改一下 function createElement( tag,...children ) { // 如果tag是一个方法,那么它是一个组件 if ( typeof tag === 'function' ) { return tag( attrs || {} ); } return { tag,children } } 渲染函数定义组件在简单的修改了 const element = <Welcome name="Sara" />; ReactDOM.render( element,document.getElementById( 'root' ) ); 在浏览器中可以看到结果:
试试更复杂的例子,将多个组件组合起来: function App() { return ( <div> <Welcome name="Sara" /> <Welcome name="Cahal" /> <Welcome name="Edite" /> </div> ); } ReactDOM.render( <App />,document.getElementById( 'root' ) ); 在浏览器中可以看到结果: 类定义组件类定义组件相对麻烦一点,我们通过继承 class Welcome extends React.Component { render() { return <h1>Hello,{this.props.name}</h1>; } } Componet为了实现类定义组件,我们需要定义一个 class Component {} state & props通过继承 // React.Component class Component { constructor( props = {} ) { this.isReactComponent = true; this.state = {}; this.props = props; } } 这里多了一个 setState组件内部的 import ReactDOM from '../react-dom' class Component { constructor( props = {} ) { // ... } setState( stateChange ) { // 将修改合并到state Object.assign( this.state,stateChange ); if ( this._container ) { ReactDOM.render( this,this._container ); } } } 你可能听说过React的 让createElemen支持类定义组件在js中, function createElement( tag,...children ) { // 类定义组件 if ( tag.prototype && tag.prototype.render ) { return new tag( attrs ); // 函数定义组件 } else if ( typeof tag === 'function' ) { return tag( attrs || {} ); } return { tag,children } } render函数定义组件返回的是jsx,我们不需要做额外处理。但是类定义组件不同,它并不直接返回jsx。而是通过 所以我们需要修改 function render( vnode,container ) { if ( vnode === undefined ) return; // 当vnode为字符串时,渲染结果是一段文本 if ( typeof vnode === 'string' ) { const textNode = document.createTextNode( vnode ); return container.appendChild( textNode ); } const dom = document.createElement( vnode.tag ); if ( vnode.attrs ) { Object.keys( vnode.attrs ).forEach( key => { if ( key === 'className' ) key = 'class'; // 当属性名为className时,改回class dom.setAttribute( key,vnode.attrs[ key ] ) } ); } vnode.children.forEach( child => render( child,dom ) ); // 递归渲染子节点 return container.appendChild( dom ); // 将渲染结果挂载到真正的DOM上 } 在上文定义 function render( vnode,container ) { if ( vnode.isReactComponent ) { const component = vnode; component._container = container; // 保存父容器信息,用于更新 vnode = component.render(); // render()返回的结果才是需要渲染的vnode } // 后面的代码不变... } 现在我们的render方法就可以用来渲染组件了。 生命周期上面的实现还差一个关键的部分:生命周期。 在React的组件中,我们可以通过定义生命周期方法在某个时间做一些事情,例如定义 但是现在我们的实现非常简单,还没有对比虚拟DOM的变化,很多生命周期的状态没办法区分,所以我们暂时只添加 function render( vnode,container ) { if ( vnode.isReactComponent ) { const component = vnode; if ( component._container ) { if ( component.componentWillUpdate ) { component.componentWillUpdate(); // 更新 } } else if ( component.componentWillMount ) { component.componentWillMount(); // 挂载 } component._container = container; // 保存父容器信息,用于更新 vnode = component.render(); } // 后面的代码不变... } 渲染类定义组件现在大部分工作已经完成,我们可以用它来渲染类定义组件了。 class Welcome extends React.Component { render() { return <h1>Hello,{this.props.name}</h1>; } } class App extends React.Component { render() { return ( <div> <Welcome name="Sara" /> <Welcome name="Cahal" /> <Welcome name="Edite" /> </div> ); } } ReactDOM.render( <App />,document.getElementById( 'root' ) ); 运行起来结果和函数定义组件完全一致: 再来尝试一个能体现出类定义组件区别的例子,实现一个计数器 class Counter extends React.Component { constructor( props ) { super( props ); this.state = { num: 0 } } componentWillUpdate() { console.log( 'update' ); } componentWillMount() { console.log( 'mount' ); } onClick() { this.setState( { num: this.state.num + 1 } ); } render() { return ( <div onClick={ () => this.onClick() }> <h1>number: {this.state.num}</h1> <button>add</button> </div> ); } } ReactDOM.render( <Counter />,document.getElementById( 'root' ) ); 可以看到结果: mount只在挂载时输出了一次,后面每次更新时会输出update 后话至此我们已经从API层面实现了React的核心功能。但是我们目前的做法是每次更新都重新渲染整个组件甚至是整个应用,这样的做法在页面复杂时将会暴露出性能上的问题,DOM操作非常昂贵,而为了减少DOM操作,React又做了哪些事?这就是我们下一篇文章的内容了。 这篇文章的代码:https://github.com/hujiulong/... 从零开始实现React系列React是前端最受欢迎的框架之一,解读其源码的文章非常多,但是我想从另一个角度去解读React:从零开始实现一个React,从API层面实现React的大部分功能,在这个过程中去探索为什么有虚拟DOM、diff、为什么setState这样设计等问题。 整个系列大概会有六篇左右,我每周会更新一到两篇,我会第一时间在github上更新,有问题需要探讨也请在github上回复我~ 博客地址: https://github.com/hujiulong/blog 上一篇文章从零开始实现React(一):JSX和虚拟DOM (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |