• 前言

    最近在深入学习React的过程中,逐步摸索出了一点新东西,让自己对这套框架有了一点新的理解。开始自己写出一点带有“黑魔法”的代码,慢慢体会到了其中神奇的奥秘~

深入几个基本概念

  • ReactNode
    	type ReactText = string | number;
    	type ReactChild = ReactElement | ReactText;
    
    	interface ReactNodeArray extends Array<ReactNode> {}
    	type ReactFragment = {} | ReactNodeArray;
    
    	type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;
    

    从以上React的源码中可以看到,ReactNode是一群对象的集合,几乎可以为任何东西。

  • ReactElement
    	type Key = string | number
    
    	interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> {
    	  type: T;
    	  props: P;
    	  key: Key | null;
    	}
    

    ReactElement则是具有type和props两个属性的一个对象。我们知道,在虚拟DOM中,一个节点就具有type,props,key这些属性。因此,ReactElement一定和v-DOM有很大关系。

  • JSX.Element
    其实React就是让JSX.Element和ReactElement等价,因为JSX.Element在js编译器执行后得到的对象就是虚拟DOM,我的理解下,操作ReactElement就等价操作虚拟DOM。

探究JSX语法

  • 在我们日常开发中,一个JSX/TSX文件是一个页面或者一个组件,但是我们在拼装这些页面和组件的最后,需要将拼好的一个完整页面渲染到DOM中,把我们的JSX转换为v-DOM和DOM。一个组件或者页面我们在函数写法中会像下面这样写,接收一组props,返回一个ReactElement或者ReactNode。
    	export default function Demo(props: IProps) {
    	  // values and hooks
    	  return <div>demo</div>
    	}
    
    
  • 注意一下props和返回值,不同于Vue这样的框架,虽然我们返回的一个ReactElement看上去像一个HTML模板,但是它实际上可以是任何东西,因为它最终会被解析为虚拟DOM;而props,就更没有东西来做限制了,完全可以由我们自定义接收什么样的值。因此,我们可以在这两个地方做出一点“不走寻常路”的东西。

一个灵感的借鉴

  • 在之前的开发中,我使用了一个叫react-beautiful-dnd的组件库。他是一个可以用来快速构建拖拽交互的组件,其中的一个写法让我刚接触react的时候比较迷惑,大概就是在return的东西里面,有这样一个元素:
    	<Dragable>
    	(provided,snapshot) => (
    		<Dropable>
    			xxx
    		</Dropable>
    	)
    	</Dragable>
    
  • 当时完全无法理解为什么两个标签一样的东西中间可以夹带一个看上去像函数的玩意。直到有一次因为某个需求的驱动下去翻了翻他的源码,才发现其中的奥秘:
    	export interface DroppableProps {
    	    droppableId: DroppableId;
    	    type?: TypeId;
    	    mode?: DroppableMode;
    	    isDropDisabled?: boolean;
    	    isCombineEnabled?: boolean;
    	    direction?: Direction;
    	    ignoreContainerClipping?: boolean;
    	    renderClone?: DraggableChildrenFn;
    	    getContainerForClone?: () => React.ReactElement<HTMLElement>;
    	    children(provided: DroppableProvided, snapshot: DroppableStateSnapshot): React.ReactElement<HTMLElement>;
    	}
    
    	export class Droppable extends React.Component<DroppableProps> { }
    

    props中,有一个children属性,它是一个接收两个参数,返回了ReactElement的函数。而这个children,在我们编写的代码中,正是夹在两个看着像标签一样的东西中间的玩意。在渲染时,外层的组件把这两个参数传入这个函数,然后由内层的组件去接收,然后做出相应的处理并返回一个ReacteElement。

项目中的实践

  • 看懂了这个写法,我们就可以很轻松地对一个组件进行“套娃”调用:

    	interface IProps {
    	  father: any
    	  children(father: any): ReactElement
    	}
    
    	export default function Demo(props: IProps) {
    	  const { father, children } = props
    	  return <div>{children(father)}</div>
    	}
    

    父组件中:

    	export default function App() {
    	  return (
    	    <div>
    	      <Demo>
    	        {(father: any) => (
    	          <Demo>
    	            {(father: any) => <Demo>{(father: any) => <Demo />}</Demo>}
    	          </Demo>
    	        )}
    	      </Demo>
    	    </div>
    	  )
    	}
    

    这样可以让我们在递归渲染每个节点的时候,都知道父节点的状态,以此来做一些额外的操作。这种写法的典型应用场景就是在展开一个json树的时候,我们希望它的每个节点都是一个组件,并且可以由父组件控制。配合上一些全局的状态管理,我们能做到的事情就更多了。

  • 最后

    一些参考的资料:
    组合和继承(类似于slot)
    几种类型的区别
    react-beautiful-dnd


What is broken can be reforged.