写在开头:
去年发表过一篇手写React,带diff算法,异步setState队列的文章,有一位阿里的朋友在下面评论,让我可以用hooks实现一次,也很简单,我当时觉得,这人有病,现在回过头来看,还是补上吧,世间万物都逃不过真香定律
往期精彩文章:
原创:从零实现一个简单版React (附源码)
如何优化你的超大型React应用 【原创精读】
深度:手写一个WebSocket协议 [7000字]
干货:深入了解React 渲染原理及性能优化
精读:10个案例让你彻底理解React hooks的渲染逻辑
我之前写的React gitHub地址是:
https://github.com/JinJieTan/mini-react/tree/diff-async
仓库分支 从master => diff => diff-async 逐步变得完善
目前hooks刚开始实现,在我的React源码中,每个入口中预留了hooks
import handleAttrs from './handleAttrs';import { createComponent, setComponentProps } from '../components/utills';const ReactDom = {};//传入虚拟dom节点和真实包裹节点,把虚拟dom节点通过_render方法转换成真实dom节点,然后插入到包裹节点中,这个就是react的初次渲染const render = function(vnode, container) { return container.appendChild(_render(vnode));};ReactDom.render = render;export function _render(vnode) { console.log(vnode); console.log('_render '); if (vnode === undefined || vnode === null || typeof vnode === 'boolean') vnode = ''; if (typeof vnode === 'number') vnode = String(vnode); if (typeof vnode === 'string') { let textNode = document.createTextNode(vnode); return textNode; } if (typeof vnode.tag === 'function') { //hooks const component = createComponent(vnode.tag, vnode.attrs); setComponentProps(component, vnode.attrs); return component.base; } // vnode= {tag,props,children} // {tag:"li",attrs:{xxx:},children:1} const dom = document.createElement(vnode.tag); if (vnode.attrs) { Object.keys(vnode.attrs).forEach(key => { const value = vnode.attrs[key]; handleAttrs(dom, key, value);//如果有属性,例如style标签、onClick事件等,都会通过这个函数,挂在到dom上 }); } vnode.children && vnode.children.forEach(child => render(child, dom)); // 递归渲染子节点 return dom;}export default ReactDom;
要开始完善hooks部分了我们现在,从25行代码开始
我们先把启动入门的文件换成hook
import React from './react';import ReactDom from './reactDom';import App from './app';import Hook from './hook';ReactDom.render(, document.querySelector('#root'));
开发一个简单的hook组件
import React from 'react';export default function hook(props) { console.log(props, 'props'); return <div>hooksdiv>;}
然后启动项目
parcel index.html
然后访问 http://localhost:1234
发现什么都没有,因为之前对hooks并没有做什么处理
export function createComponent(component, props) { console.log(component, props, '1'); let inst; // 如果是类定义组件,则直接返回实例 if (component.prototype && component.prototype.render) { inst = new component(props); // 如果是函数定义组件,则将其扩展为类定义组件 } else { inst = new Component(props); inst.constructor = component; inst.render = function () { return this.constructor(props); }; } console.log(inst.render(), 'render'); return inst;}
我们现在,这里发现如果是一个函数式组件,那么就构造调用它,然后确保原型之后,扩展成类组件。
这里15行代码断点日志可以看到,已经可以获得虚拟DOM了
继续回到入口这里
if (typeof vnode.tag === 'function') { //hooks const component = createComponent(vnode.tag, vnode.attrs); setComponentProps(component, vnode.attrs); return component.base; }
在将函数组件拓展成类组件后,进行setComponentProps的一些操作,最终返回真实dom,即component.base,被插入到root节点中,完成渲染
export function renderComponent(component) { //dom console.log('renderComponent',component); let base; //返回虚拟dom对象 调用render方法,会用到state 此时的state已经通过上面的队列更新了 const renderer = component.render(); if (component.base && component.componentWillUpdate) { component.componentWillUpdate(); } if (component.base && component.shouldComponentUpdate) { let result = true; result = component.shouldComponentUpdate && component.shouldComponentUpdate( (component.props = {}), component.newState ); if (!result) { return; } } //得到真实dom对象 base = diffNode(component.base, renderer); console.log(base,'base') if (component.base) { if (component.componentDidUpdate) component.componentDidUpdate(); } else { component.base = base; base._component = component; component.componentDidMount && component.componentDidMount(); return; } //挂载真实的dom对象到对应的 组件上 方便后期对比 component.base = base; //挂载对应到组件到真实dom上 方便后期对比~ base._component = component;}
这里我们可以看到,hook组件传入后的打印:
base是undefined,因为我们目前没有对hook的diff这些做处理,所以真实dom为undefined,这样屏幕上没有任何元素显示
由于hook的逻辑跟class组件实现逻辑是不太一样,里面很多是依赖链表、数组去实现的,所以我们针对这个地方,要单独写一套逻辑,如果是hook组件的话,我们要重新做一套解析
if (typeof vnode.tag === 'function') { //hooks const component = createComponent(vnode.tag, vnode.attrs); const isHook = true; setComponentProps(component, vnode.attrs, isHook); return component.base; }
在这里我们加入isHook字段,传入。这样后续就知道我们是hook组件了,应该如何对待
如下所示:
这里的核心是diff算法实现,之前我是把真实dom节点和虚拟dom节点去对比的:
//得到真实dom对象 base = diffNode(component.base, renderer);
这里我们今天先把它渲染出来,然后实现useState这些核心的东西。下一期我再加入diff算法,这样阅读也更友好(主要是太晚了,明天还要上班)
此时发现,是可以得到我的tag标签,以及children内容,那么就可以展示了
我只用了十行不到的代码就实现了hooks组件展示
达到预期(虽然目前没有预期,递归diff、展示等)
接下来先看看React怎么实现的useState。
hooks里,有一个getHookState函数,会在当前组件的实例上挂载__hooks属性。__hooks为一个对象,__hooks对象中的_list属性使用数组的形式,保存了所有类型hooks(useState, useEffect…………)的执行的结果,返回值等。因为_list属性是使用数组的形式存储状态,所以每一个 hooks 的执行顺序尤为重要。
function getHookState(index) { if (options._hook) options._hook(currentComponent); // 检查组件,是否有__hooks属性,如果没有,主动挂载一个空的__hooks对象 const hooks = currentComponent.__hooks || (currentComponent.__hooks = { _list: [], // _list中存储了所有hooks的状态 _pendingEffects: [], // _pendingEffects中存储了useEffect的state _pendingLayoutEffects: [], // _pendingLayoutEffects中存储了useLayoutEffects的state _handles: [] }); // 根据索引index。判断__hooks._list数组中,是否有对应的状态。 // 如果没有,将主动添加一个空的状态。 if (index >= hooks._list.length) { hooks._list.push({}); } // 返回__hooks._list数组中,索引对应的状态 return hooks._list[index];}
hook的执行指针
// 当前hooks的执行顺序指针let currentIndex;// 当前的组件的实例let currentComponent;let oldBeforeRender = options._render;// vnode是options._render = vnode => { if (oldBeforeRender) oldBeforeRender(vnode); // 当前组件的实例 currentComponent = vnode._component; // 重置索引,每一个组件hooks state list从0开始累加 currentIndex = 0; if (currentComponent.__hooks) { currentComponent.__hooks._pendingEffects = handleEffects( currentComponent.__hooks._pendingEffects ); }};
我需要模仿它,用链表实现useState的效果,由于useState在源码中其实是依赖useReducer实现,这点在.d.ts源码可以看到
// useState接受一个初始值initialState,初始化statefunction useState(initialState) { return useReducer(invokeOrReturn, initialState);}
useState基于useReducer实现,invokeOrReturn是一个简单工具函数
function invokeOrReturn(arg, f) { return typeof f === "function" ? f(arg) : f;}
useReducer:
function useReducer(reducer, initialState, init) { // currentIndex自增一,创建一个新的状态,状态会存储在currentComponent.__hooks._list中 const hookState = getHookState(currentIndex++); if (!hookState._component) { // state存储当前组件的引用 hookState._component = currentComponent; hookState._value = [ // 如果没有指定第三个参数`init, 返回initialState // 如果指定了第三个参数,返回,经过惰性化初始值的函数处理的initialState // `useState`是基于`useReducer`的封装。 // 在`useState`中,hookState._value[0],默认直接返回initialState !init ? invokeOrReturn(null, initialState) : init(initialState), // hookState._value[1],接受一个`action`, { type: `xx` } // 由于`useState`是基于`useReducer`的封装,所以action参数也可能是一个新的state值,或者state的更新函数作为参数 action => { // 返回新的状态值 const nextValue = reducer(hookState._value[0], action); // 使用新的状态值,更新状态 if (hookState._value[0] !== nextValue) { hookState._value[0] = nextValue; // ⭐️调用组件的setState, 重新进行diff运算(在Preact中,diff的过程中会同步更新真实的dom节点) hookState._component.setState({}); } } ]; } // 对于useReduer而言, 返回[state, dispath] // 对于useState而言,返回[state, setState] return hookState._value;}
正式开始:
import { Component } from '../components/component';import useState from './useState';const React = {};React.Component = Component;export const myUseState = useState;React.createElement = function (tag, attrs, ...children) { return { tag, attrs, children, };};export default React;
在React中加入useState模块,第一个版本,做简单点,容易阅读理解上手。一步到位读写都太累
const useState = (initialState) => { return [value, setValue];};export default useState;
我们在useState使用时,传入初始值,然后返回一个数组,提供后续数组结构赋值使用
const myUseState = (initialState) => { console.log(this, 'this'); let value = initialState || null; const setValue = (newValue) => { value = newValue; }; return [value, setValue];};export default myUseState;
这个初始版本实现很容易,我们先不考虑其他场景,这个乞丐版,如何跑起来
现在组件内已经可以看到useState了
import React, { myUseState } from './react';export default function hook(props) { const [value, setValue] = myUseState(1); console.log(props, 'props', myUseState, this); return <div>hooksdiv>;}
打印效果:
可是如何跟组件重新渲染逻辑挂钩呢?这里可能需要使用到链表了,但是我们是先实现乞丐版,并不追求性能,要求就是能用即可。接下来我会思考,如何将hook和组件内的渲染结合在一起,其实今天是太监了,本来想写完的,太晚了。先思考一个好的设计,五一回来后开始实现Hooks。想要一起开发,PR形式提交代码也可以~
最后
欢迎加我微信(CALASFxiaotan),拉你进技术群,长期交流学习...
欢迎关注「前端巅峰」,认真学前端,做个有专业的技术人...
好文我在看?