首先回顾react的功能
1:虚拟dom,diff算法;
2:组件化,分为函数组件与类组件
3:类中有setState函数中有Hooks
4:生命周期
5:state的异步更新
首先最基本使用
import React from './react';
import ReactDOM from './react-dom';
class Home extends React.Component {
render(){
let { num } = this.state;
return (
<div className="home">
我是类组价{num}
<button onClick={()=>this.hanldClick()}>点击更新</button>
</div>
)
}
}
ReactDOM.render(<Home name={"我是组件"}/>,document.getElementById('root'));
其中render中的标签是jsx的虚拟dom,这里使用babel转义了
开始项目前的配置
//webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
publicPath: '/dist',
filename: 'bunrl.js'
},
module:{ // 第三方模块配置规则
rules:[
{test:/\.js|jsx$/ , use:{
loader:'babel-loader',
options: {
presets:['@babel/preset-env']}
}, exclude: /node_modules/} // 添加排除项
]
},
devServer: {
open: true,
// openPage: './main.html',
}
}
//.babelrc中配置
{
"presets":["@babel/env", "@babel/react"],
"plugins":["@babel/plugin-transform-runtime"]
}
//package.json
{
"name": "react-sources",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "webpack-dev-server",
"build":"webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.15.0",
"@babel/plugin-transform-runtime": "^7.15.0",
"@babel/preset-env": "^7.15.0",
"@babel/preset-react": "^7.14.5",
"babel-loader": "^8.2.2",
"webpack": "^5.50.0",
"webpack-cli": "^3.3.12"
},
"dependencies": {
"webpack-dev-server": "^3.11.2"
}
}
配置完成
虚拟dom,diff算法
配置完启动项目会报一个react.createElement() is not function这个正常因为使用jsx的语法转义就是createElement的语法糖
定义createElement方法src下创建react目录下面创建index文件
/src/react/index.js
function createElement(tag,attrs,...childrens){
attrs = attrs||{};
return {
tag,//外层的标签
attrs,//属性,是一个对象
childrens,//是一个数组
key:attrs.key||null
}
}
export default {
createElement
};
这里的createElement接受三个参数节点类型,属性,子节点,标签上需要key值先定义,最后返回
随便写个标签测试一下返回如下,但是这些都是虚拟dom我们需要将这些转换为真实dom
先随便写个标签测试一下,先渲染最简单的dom
模仿react的做法这里是这样用的我们也定义src目录下创建react-dom/index.js
里面有render方法定义,这里我们直接加入diff想一步步来看下面的原文章,但是源文章没有实现hooks
//第一个是组件,第二个是根节点,渲染完成后加入dom
//vnode是选你dom{tag,childrne,attrs},根节点root,dom起初是undefined,是后面要添加的新dom
function render(vnode,container,dom){
//转交diff进行遍历,返回之后渲染到
return diff(dom,vnode,container);
}
react-dom下新建diff.js文件
//diff.js
//调用diff遍历
export function diff(dom,vnode,container){
//首先调用diffNode生成dom节点
const ret = diffNode(dom,vnode);
//ret是返回新的dom,
//首先判断是否有根节点
if(container){
//然后挂载到根节点
container.appendChild(ret);
}
return ret;
}
现在只要获取到真实的dom添加到更节点就可以了,定义diffNode生成真实dom
/**生成dom,或更新dom,使用真实dom和虚拟dom对比 */
export function diffNode(dom,vnode){
let out = dom;
//判断虚拟dom类型是undefined,null,boolean,直接返回字符串
if(vnode===undefined||vnode===null||typeof vnode==="boolean") vnode="";
//判断如果为number转为stirng类型
if(typeof vnode==='number') vnode = String(vnode);
//如果VNode是字符串,
if(typeof vnode === "string"){
//创建文本节点
if(dom&&dom.nodeType===3){
//比较
if(dom.textContent !== vnode) {
//更新文本内容
dom.textContent = vnode;
}
}else{
out = document.createTextNode(vnode);
if(dom&&dom.parentNode){
dom.parentNode.replaceNode(out,dom);
}
}
return out;
}
//创建非文本dom节点,初始为undefined
if(!dom){
//创建dom节点,就是最外层的dom
out = document.createElement(vnode.tag);
}
//判断属性是否相同,添加属性
diffAttribute(out,vnode);
//比较子节点(dom节点和组件),判断是否有子节点
if(vnode.childrens && vnode.childrens.length>0||(out.childrens&&out.childNodes.length>0)){
//对比组件,或者子节点
diffChildren(out,vnode.childrens);
}
return out;
}
我们对虚拟dom进行遍历,判断是文本,数字等类型进行处理,外层如果是文本创建直接返回,
,由于刚开始dom可能是undefined,我们给他创建外层标签,外层一定有标签就是vnode.tag,创建完了之后就可以对比属性了
/**处理属性的,如果有属性比较,没有添加 */
function diffAttribute(dom,vnode){
//保存之前dom所有属性
let oldAttrs = {};
let newAttrs = vnode.attrs;
//dom是原有节点对象,vnode是虚拟dom
const domAttrs = dom.attributes;
[...domAttrs].forEach(item=>{
oldAttrs[item.name] = item.value;
})
//比较
//如果原来的属性跟新的属性对比,不在新的属性中,则将其移除(属性为undefined)
for(let key in oldAttrs){
//没有在新的属性中返回false取反删除属性
if(!(key in newAttrs)){
//这里对比属性名不对比属性值
setAttribute(dom,key,undefined);
}
}
//对比值是否相同
for(let key in newAttrs){
//这里对比属性值,新与旧对比不相同,将新的复制给这个dom
if(newAttrs[key]!==oldAttrs[key]){
//值不同更新值
setAttribute(dom,key,newAttrs[key]);
}
}
}
属性对比对比就是对比属性进行替换,没有就删除
子节点
以上我们只是对比的dom如果是一个单独的jsx如果里面还有嵌套的话就循环添加dom
/**处理子节点渲染,vChildrends是子节点*/
function diffChildren(dom, vchildren) {
const domChildren = dom.childNodes;
const children = [];
const keyed = {};
// 将有key的节点(用对象保存)和没有key的节点(用数组保存)分开
if (domChildren.length > 0) {
[...domChildren].forEach(item => {
// 获取key
const key = item.key;
if (key) {
// 如果key存在,保存到对象中
keyed[key] = item;
} else {
// 如果key不存在,保存到数组中
children.push(item)
}
})
}
if (vchildren && vchildren.length > 0) {
let min = 0;
let childrenLen = children.length; //2
[...vchildren].forEach((vchild, i) => {
// 获取虚拟DOM中所有的key
const key = vchild.key;
let child;
if (key) {
// 如果有key,找到对应key值的节点
if (keyed[key]) {
child = keyed[key];
keyed[key] = undefined;
}
} else if (childrenLen > min) {
// alert(1);
// 如果没有key,则优先找类型相同的节点
for (let j = min; j < childrenLen; j++) {
let c = children[j];
if (c) {
child = c;
children[j] = undefined;
if (j === childrenLen - 1) childrenLen--;
if (j === min) min++;
break;
}
}
}
// 对比
child = diffNode(child, vchild);
// 更新DOM
const f = domChildren[i];
if (child && child !== dom && child !== f) {
// 如果更新前的对应位置为空,说明此节点是新增的
if (!f) {
dom.appendChild(child);
// 如果更新后的节点和更新前对应位置的下一个节点一样,说明当前位置的节点被移除了
} else if (child === f.nextSibling) {
removeNode(f);
// 将更新后的节点移动到正确的位置
} else {
// 注意insertBefore的用法,第一个参数是要插入的节点,第二个参数是已存在的节点
dom.insertBefore(child, f);
}
}
})
}
}
首次进入肯定不会进入第一个判断,diff只会在更新的时候才会使用,初始渲染,挂载都不会触发
进入第二个判断渲染dom,继续判断是否有key值,有的话添加keyed保存,没有直接循环渲染
没有key的里面都不会执行,直接调用diffnode返回真实dom,一下就是将真实dom插入到对应dom中
组件
dom完了开始组件的更新与渲染
diffNode中加一条判断
//如果是组件
if(typeof vnode.tag==="function"){
//如果是组件传入dom,与vnode
return diffComponent(out,vnode);
}
/**处理组件 */
function diffComponent(dom,vnode){
//dom就是传入的out,
let comp = dom;
//如果组件没有变化,
if(comp&&comp.constructor === vnode.tag){
//设置props
setComponentProps(comp,vnode.attrs);
dom = comp.base;
}else {
//组件类型发生变化
if(comp){
//先移除旧的组件
unmountComponent(comp);
comp = null;
}
//1.创建新组件
comp = createComponent(vnode.tag,vnode.attrs);
//2.设置组件属性
setComponentProps(comp,vnode.attrs);
//3.当前挂载base赋值到dom
dom = comp.base;
}
return dom;
}
react-dom/index.js
/**
* 创建组件
*/
export function createComponent(comp,props){
let inst;
//判断是函数还是类组件
if(comp.prototype&&comp.prototype?.render){
//类组件创建实例返回
inst = new comp(props);
}else{
//如果是函数是组件,扩展成类组件,方便后面统一管理
inst = new Component(props);
inst.constructor = comp;
//定义render函数
inst.render = function(){
return this.constructor(props);
}
}
return inst;
}
react-dom/index.js
/**
* 设置组件属性
*/
export function setComponentProps(comp,props){
//组件挂载之前
if(!comp.base){
if(comp.componentWillMount) comp.componentWillMount();
}else{
if(comp.componentWillReceiveProps) comp.componentWillReceiveProps();
}
//设置组件属性
comp.props = props;
//渲染组件
renderComponent(comp);
}
react-dom/index.js
/**
* 渲染组件
*/
export function renderComponent(comp){
//调用render方法,返回虚拟dom
const renderer = comp.render();
let base;
//调用diffNode返回真实dom
base = diffNode(comp.base,renderer);
//首次进入组件没有comp.base
if(comp.base&&comp.componentWillUpdata) comp.componentWillUpdata();
if(comp.base) {
if(comp.componentDidupdata) comp.componentDidupdata();
//如果不是初次进入comp.componentDidMount();只执行一次
}else if(comp.componentDidMount){
comp.componentDidMount();
}
//挂载dom节点
comp.base = base;
}
转换组件
import { renderComponent } from '../react-dom';
export default class Component{
constructor(props={}){
this.props = props;
this.state={}
}
setState(stateChange){
//添加入队,出对
enqueueSetState(stateChange,this);
}
}
// 创建队列
const setStateQueue = [];//保存当前state(函数或值)和组件
const renderQueue = [];//保存组件
// 队列: 先进先出
export function enqueueSetState(stateChange, component) {
// 如果setStateQueue的长度是0,也就是在上次flush执行之后第一次往队列里添加
if (setStateQueue.length === 0) {
defer(flush);
}
// 保存到队列中
setStateQueue.push({
stateChange,
component
})
// 如果renderQueue里没有当前组件,则添加到队列中
let r = renderQueue.some(item => {
return item === component;
})
//添加
if (!r) {
renderQueue.push(component);
}
}
function flush() {
let item,component;
//遍历state,调用队列获取返回值item有值继续调,没有不继续
while(item=setStateQueue.shift()){
//结构获取值
const {stateChange,component} = item;
// 如果没有prevState,则将当前的state作为初始的prevState
if (!component.prevState) {
//合并state赋值
component.prevState = Object.assign({}, component.state);
}
// 如果stateChange是一个方法,也就是setState的第一种形式
if (typeof stateChange === 'function') {
//调用函数,将prevState传入,还有props传入,返回结果合并到当前state
Object.assign(component.state, stateChange(component.prevState, component.props))
} else {
// 如果stateChange是一个对象,则直接合并到setState中
Object.assign(component.state, stateChange);
}
//重新赋值
component.prevState = component.state;
}
//遍历组件
while(component = renderQueue.shift()){
//渲染组件
renderComponent(component);
}
}
//接受一个函数,返回一个异步执行的函数
function defer( fn ) {
return Promise.resolve().then( fn );
}
hooks
const hooks = (function () {
//初始化全部变量
const HOOKS = [];
let currentIndex = 0;
const Tick = {
render: null,//保存渲染函数
rootDom: null,//保存根节点
rootCompon: null,//保存根组件
queue: [],//保留函数的执行
//接受一个函数
nextTick: function (update) {
//把这个函数存入数组
this.queue.push(update);
//异步执行
Promise.resolve(() => {
//首先判断是否有参数
// 一次循环后,全部出栈,确保单次事件循环不会重复渲染
if (this.queue.length) {
this.queue.forEach(f => f()); // 依次执行队列中所有任务
//这里必须重置,下次进入的时候渲染的时候读取的是起初状态
//不重置就会导致currindex不对获取错误,保证下次进入还是第一次进入的程序获取值
currentIndex = 0; // 重置计数
this.queue = []; // 清空队列
// debugger;
this.render && this.render(this.rootCompon, this.rootDom); // 更新dom
}
}).then(f => f());
}
};
function useState(initialState) {
HOOKS[currentIndex] = HOOKS[currentIndex] || (typeof initialState === 'function' ? initialState() : initialState);
const memoryCurrentIndex = currentIndex; // currentIndex 是全局可变的,需要保存本次的
//p接受一个参数,可以是参数也可以是函数
const setState = p => {
let newState = p;
if (typeof p === 'function') {
//函数的话调用函数,将上一次的参数(HOOKS[memoryCurrentIndex)传入
newState = p(HOOKS[memoryCurrentIndex]);
}
//判断这次的值与上次的值比较,相同return
if (newState === HOOKS[memoryCurrentIndex]) return;
//修改值,接受一个函数
Tick.nextTick(() => {
//更新数据
HOOKS[memoryCurrentIndex] = newState;
});
};
//返回参数,与修改参数的方法,currentIndex+1防止覆盖,如果上面不清空这里HOOKS会不停的添加数据
return [HOOKS[currentIndex++], setState];
//如:["三笠", 1, 100, "三笠", 1, 100, "三笠", 1, 100, "三笠", 1, 100, "三笠", 1, 100, "三笠", 1, 100, "三笠", 1, 100, "三笠", 0, 100]
}
//接受两个参数函数,依赖
function useEffect(fn, deps) {
debugger;
//获取当前的数据,初次进入一定是undefined
const hook = HOOKS[currentIndex];
//_deps没有说明是初次调用
const _deps = hook && hook._deps;
//deps有值有依赖,判断这次的deps(重新渲染的依赖)与HOOKS上次的值对比是否相同,
//返回false说明变化了取反执行函数,如果返回为true说明没变,就会直接跳过执行函数
const hasChange = _deps ? !deps.every((v, i) => _deps[i] === v) : true;
const memoryCurrentIndex = currentIndex; // currentIndex 是全局可变的
//判断
if (hasChange) {
const _effect = hook && hook._effect;
//最后执行定时器,这里注意会在dom等操作执行完最后执行
setTimeout(() => {
typeof _effect === 'function' && _effect(); // 每次先判断一下有没有上一次的副作用需要卸载
//执行函数
const ef = fn();
// 更新effects,fn如果返回函数就是卸载周期(副作用),展开原来的合并
HOOKS[memoryCurrentIndex] = { ...HOOKS[memoryCurrentIndex], _effect: ef };
})
}
//保存值dep依赖
HOOKS[currentIndex++] = { _deps: deps, _effect: null };
}
return {
Tick, useState, useEffect
}
})();
export default hooks;
改一下render
src/react-dom/index.js
import Component from "../react/component";
import { diff,diffNode } from './diff';
import hooks from '../react/hook';
hooks.Tick.render = render;
//第一个是组件,第二个是根节点,渲染完成后加入dom
function render(vnode,container,dom){//vnode是选你dom{tag,childrne,attrs},根节点root,dom起初是undefined,是后面要添加的新dom
//将根组件挂载到hooks
hooks.Tick.rootCompon = vnode;
//将根元素挂载到hooks
hooks.Tick.rootDom = container;
//清空之前的dom节点
if(container.childNodes.length){
container.innerHTML="";
}
//转交diff进行遍历,返回之后渲染到
return diff(dom,vnode,container);
}
import React from './react';
import hooks from './react/hook';
import ReactDOM from './react-dom';
const { useState,useEffect } = hooks;
function App(){
const [total,setTotal] = useState(100);
return (
<div>
<button onClick={()=>setTotal(prev=>prev-1)}>降价</button>
价钱是{total}
<button onClick={()=>setTotal((prev)=>prev+1)}>抬价</button>
</div>
)
}
function Home(){
const [ state,setState ] = useState("三笠");
const [ num , setNum ] = useState(0);
useEffect(()=>{
console.log("我是第一次执行");
},[num])
return (
<div className="home">
<button onClick={()=>setNum((prev)=>prev+1)}>+</button>
<span>{num}</span><span>{state}</span>
<button onClick={()=>setNum(prev=>prev-1)}>-</button>
<App/>
</div>
)
}
ReactDOM.render(<Home name={"我是组件"}/>,document.getElementById('root'));