手搭react实现核心功能

首先回顾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'));

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值