目录
一、在了解React方面的思考
1.react是什么?
每一次思考和整理一个框架的框架或者一门语言的时候,反复问同一个问题,它是什么?每次都会有不同的答案。
首先,在之前整理的过程中,了解到了React是一个ui库。然后进一步划分react的相关技术栈,又衍生到Typescript,reduce,dva,scss,webpack,react-router等等等,单从react的用法入手,就是react常用api,react虚拟dom节点,react生命周期,react hook这些。在官方文档中可以看到大部分关于react的api描述和用法描述。然后再到react相关编程思想,相关优化思想。这些都是react。
同时在整理javascript的时候,认为react是js这个主干的分支,可以看作js库的一种。横看成岭侧成峰,不同角度思考和了解的面更广一些,理解的程度则更深刻一些。
2.该做些什么?
好了,这些都是react。那么问题又来了,看了很多对React的分析也好,再看很多遍官方文档之后,问自己,是否熟练掌握react? 诚恳的来说,那是会用,离熟练总感觉差挺远。那么是不是看了源码就能说掌握和精通?还是更感觉差的远了。既然如此,理论成立,不如实操一波。不然老是看别人分析源码来,分析源码去。不如整理好按照源码的api和基本逻辑。然后按照基本逻辑和api的作用,尝试编写一下。
基本逻辑有了,那既然是js库的一种,那就尝试写一个react框架。
react框架本身是由Facebook团队大牛整理和实现的,耗时数年。咱们的目标点,暂定在基本实现react的逻辑上。在实现了react框架的常用api后,再回过头去对比源码中的相同api的实现方式,它们考虑到的逻辑和实现的方式的区别。会理解的更深刻些,也更有味道一些。看源码解析,看来看去,都说好。还是隔着窗子在看。咱们写和别人写,差别肯定很大,但是具体差别在哪,写过之后,再去看肯定有不一样的感受。
完全可以yy一下,咱们在做的事,可以看作是react框架实现中Facebook团队的一员。在用想法和点子构建一个伟大的框架。而这个框架即将被数以亿计的开发人员使用。咳咳,此处省略500字。。。(。・∀・)ノ゙嗨就对了。
二、实现react框架的过程整理
2.1 缩小和明确需求范围
React的api中常用的和不常用的有很多,hook中通过function组件实现class组件的状态管理,生命周期。react Fiber中新的diff算法和更新机制,react自己实现的事件层等。从这些api中抽出一部分主要的骨架需求来构建原型。
1. createElement 以及ReactDom.render()也就是虚拟dom到真实dom节点的转换的功能的实现
2. 类组件和function组件转换成虚拟dom节点,再转换成真实dom节点的方式模拟
3.differ算法的基本功能的逻辑实现
3. state状态的实现,以及state状态的更新。
4.生命周期钩子函数的实现
2.2 js实现React框架的相关逻辑和对象的思考和设计
抛开代码细节不看,单从框架中对相关api的功能介绍和其它对源码解读的博客和视频来思考和整理一些数据容器及属性,以及相关的方法。 这一部分的思考另我想到了软件工程中的用例图和类图以及流程图。之前对这一块更多的是为了考试学的。但是现在来看,这些图对于需求分析和编写流程的整理有很大作用。 类图主要是是表示类的静态结构以及和其它类的关系,流程图则是输入到输入结果的闭环思考。
分别对应着编写代码时的对象和流程涉及。在捡回相关设计图的编写方式后,借用相关工具编写以后的需求逻辑。
(1)react中的虚拟dom节点操作dom节点,最后还是会回归js操作dom节点的本质。也就是document.createElement()和dom.appendChild()这些Dom对象的api调用上。既然如此,react编写dom节点的操作方式,理论上来说是编写了一套js库,这些库中的function接收了相应的参数。然后在function内部做了一系列操作,最后通过document.createElement()和dom.appendChild()方法,实现了控制dom节点变化的效果。
(2)既然是最后回归了js创建dom节点。那么dom节点可以拆分为哪几部分呢?首先就要有一个dom节点对象,然后分析dom节点对象的属性。根据传参的不同,将dom对象的实例化。最后通过js把对象转换成真实的dom节点样式。这个过程中我认为完全可以把之前看似很高端大气上档次的虚拟dom节点,用咱们刚才分析的dom节点对象替代。不管多么华丽滴外表,果然脱了都一样,咳咳。 万物皆对象蛤。
(3)state是一个状态,状态更新了,就会造成dom节点的更新。原来光记着这个框架的概念了。那么照咱们js去写逻辑怎么看它呢?state是一个对象,setstate(state)是一个方法,这个方法可以操作state对象的值,操作完了值调用逻辑(1)中的render()方法,然后执行流程(1)。这逻辑没毛病,那暂时就可以有个结论了,state是个对象,之所以能更新dom节点为什么呢?就是调用了个render方法。从基本逻辑和功能来看,state确实是这么个玩意。
(4)生命周期钩子函数。回想下react中对生命周期的介绍,刚开始学的时候,看的很神奇。组件更新,组件即将更新,组件即将挂载,组件挂载后,组件将要卸载,组件完成卸载。这么一大堆概念,又是钩子,又是生命周期。还一堆篇幅更长的博客对生命周期分析来分析去。忘掉那些牛逼Plugs。回到react的终点来看,js操作dom节点,不可否认里面有很多优化代码的精妙的代码实现方式。但是基本盘没变。有创造这么多东西吗,还是壳子太亮眼。这些概念,可以说存在,也可以说不存在。生命周期,就是function方法操作dom节点的不同的方法体内。因为function调用有顺序,而对应的funtion的功能不一样,所以生命周期函数其实就是不同的function。在不同的function内,执行对应的传参,传参也是个function。这就是钩子函数。暂时放下众多的React辞藻。瞅瞅生命周期,钩子函数。逻辑可以跑通,基本盘这样看没毛病。
这四条主线逻辑,就是开始编写React的逻辑线。各自的逻辑线肯定要向下延申,对外扩展。来实现React的更深层次的特性以及优化。
2.3 对象和function初步设计
大致的方法对象
三,开始编写实现React框架
3.1 实现dom节点对象
const app = ( //jsx语法
<div className="reacr-title" title="test">
Hello,React
<span>React</span>
</div>
)
这一部分jsx语法的转化是babel实现的最后会转化为
React.createElement("div", {
className: "reacr-title",
title: "test"
}, "Hello,React", /*#__PURE__*/React.createElement("span", null, "React"));
因此实际上咱们用的是
const React = {
createElement
}
function createElement(tag, attrs, ...childrens) {
return {
tag,
attrs,
childrens
}
}
将真实dom节点拆分属性这部分工作,babel帮我们实现了。它将dom节点对象拆分成了三部分,tag,attrs和childrens
3.2 ReactDom.render方法
const ReactDom = {//模拟ReactDom对象
render
}
// 渲染
function render(vnode,rootnode) {
if(vnode === undefined) return
if(typeof vnode === 'string'){ //1 如果vnode是字符串创建文本节点
let textNode = document.createTextNode(vnode)
return rootnode.appendChild(textNode)
}
const {tag,attrs,childrens} = vnode // 2 虚拟dom对象
const dom = document.createElement(tag)
if(attrs){
for(key in attrs){
let value = attrs[key]
setAttribute(dom,key,attrs[key])// 3.用于渲染节点的方法
}
}
}
还需要处理childrens属性
const ReactDom = {//模拟ReactDom对象
render
}
// 渲染
function render(vnode,rootnode) {
if(vnode === undefined) return
if(typeof vnode === 'string'){ //1 如果vnode是字符串创建文本节点
let textNode = document.createTextNode(vnode)
return rootnode.appendChild(textNode)
}
const {tag,attrs,childrens} = vnode // 2 虚拟dom对象
const dom = document.createElement(tag)
if(attrs){
for(key in attrs){
let value = attrs[key]
setAttribute(dom,key,attrs[key])// 3.用于渲染节点的方法
}
}
childrens&&(childrens.forEach(child=>render(child,dom)))
return rootnode.appendChild(dom);
}
setAttrbute方法
function setAttribute(dom,key,value){
// className
if(key==='className'){
key = 'class'
}
// event
if(/on\w+/.test(key)){
key = key.toLowerCase()
dom[key] = value || ''
}else if(key === 'style'){
// style 可以是字符串也可以是对象
if(!value || typeof key === 'string'){
dom.style.cssText = value || ''
}else if(value && typeof key === 'object'){
for(let k in value){
if(typeof value[k] === 'number'){
dom.style[k] = value[k] + 'px'
}else{
dom.style[k] = value[k]
}
}
}
}else{
// 其他属性
dom.setAttribute(key,value)
}
3.3 class组件和function组件渲染
function组件是return了一个和节点一样的jsx对象。那么也等同于jsx语法对象。
但是class类组件如何判断并转换成节点呢? class里面一定有一个render. 组件渲染,组件对象和节点对象不一样,存在props和state。因此要创建对应的组件对象。
(1)class类对象的原型方法中存在render。 class语法糖最后还是会转换成function方法
if(comp.prototype && comp.prototype.render)//判断是否为class组件
{}
(2)class类对象模拟
class Component{
constructor(props={}){
this.props = props
this.state = {}
}
}
const React = {
createElement,
Component
}
const ReactDom = {
render
}
(3)完成class类和function组件逻辑的实现
在原来的基础上,增加对函数组件和类组件的判断。以及函数组件对象的创建和函数组件对象属性的设置。
function render(vnode){
if(vnode === undefined || vnode === null || typeof vnode === 'boolean') vnode = ''
if(typeof vnode === 'string'){
return document.createTextNode(vnode)
}
// 如果tag是函数,渲染组件
if(typeof vnode.tag === 'function') {
// 1.创建组件
const comp = createComponent(vnode.tag,vnode.attrs)
// 2.设置组件的属性
setComponentProps(comp,vnode.attrs)
// 3.组件渲染的节点对象返回
return comp.base
}
const {tag,attrs,childrens} = vnode
const dom = document.createElement(tag)
if(attrs){
Object.keys(attrs).forEach(key=>{
const value = attrs[key]
setAttribute(dom,key,attrs[key])
})
}
childrens.forEach(child=>{
dom.appendChild(_render(child))
})
return dom
}
function createComponent(comp,props){
let inst
if(comp.prototype && comp.prototype.render){ // 判断类组件或者函数组件
inst = new comp(props)
}else{
// 函数组件扩展成类组件
inst = new Component(props)
inst.constructor = comp
inst.render = function(){
return this.constructor(props)
}
}
return inst
}
babel转换后,tag为对应的节点类型,attrs为对应的属性。
思考到这里又有一个疑惑了,那怎么实现组件间的单向数据流呢。猜测所谓的单向数据流也是调用方法传递的数据,测试下这个想法吧
/*用于测试数据传值*/
class Home extends React.Component{
render(){
return (
<div className="active" title="123">
Hello,
<span>React</span>
<test {...props}/>
</div>
)
}
}
function test(props){
return <div>
{props}
</div>
}
/*经过babel转换器转换后的结果*/
class Home extends React.Component {
render() {
return /*#__PURE__*/React.createElement("div", {
className: "active",
title: "123"
}, "Hello,", /*#__PURE__*/React.createElement("span", null, "React"), /*#__PURE__*/React.createElement("test", props));
}
}
function test(props) {
return /*#__PURE__*/React.createElement("div", null, props);
}
结果没毛病,props就是作为一个参数向下传递给另一个函数。那咱们编写的函数也能整出单向数据流来。就传参,当然向下传。
继续实现对应的组件对象的属性设置
function setComponentProps(comp,props){ //设置组件实例的属性
comp.props = props
renderComponent(comp)//
}
// 设置完成props,调用组件实例的render方法返回jsx,使用render渲染jsx
function renderComponent(comp){
let vnode = comp.render(comp.props)
comp.base = render(comp.render(comp.props))
}
类组件和function最后return了一个组件对象。实现了组件渲染节点,以及组件数据流传递的基本功能。
3.4 state更新
state在react中很重要,和组件的渲染密切相关。state变化了,对应的节点也要重新渲染。意味着setstate方法调用的同时要调用render(vnode)方法。setstate并非同步,而是会将在一段时间内的state更新状态,一起执行更新。
在框架中对state的描述是这样的。 按照相同的描述,编写对应的功能。
class Component{
constructor(props={}){
this.props = props
this.state = {}
}
}
const React = {
createElement,
Component
}
const ReactDom = {
render
}
现在组件对象中有了state
(1) setState方法
不考虑异步和一段时间内多次更新state的复杂情况
setState(stateChange) {
// 对象复制
Object.assign(this.state, stateChange)
渲染对象
renderComponent(this) //调用重新渲染的方法
}
}
通过当前组件对象的实例调用setState,更新state后,传入当前组件实例到渲染方法中。 实现组件的重新渲染
四、留待解决的问题
(1)实现React框架中setstate中多次setstate合并为一次的需求
(2)按照differ算法的基本逻辑实现differ算法的基本功能
(3)编写中实现React框架中hook中的常用api。