likes
comments
collection
share

手写React(三)实现setState及类组件更新

作者站长头像
站长
· 阅读数 52

从零到1,手写实现react!

本文接着前面的手写类组件+函数组件,继续实现类组件的更新

传送

还是那句话:

网上那些八股文,都是别人对某个知识点掌握后整理的描述,背下来没有任何意义!代码是数学题,不是背课文!

自己写一遍,彻底搞清楚它的实现过程原理,这样收获的才是自己的!用自己的理解总结出来的,才是真的掌握了!知其然,知其所以然!

我这个一步步手写实现React的笔记,希望可以帮到你。

一、前言

在开始手写之前,你可能需要了解以下几个知识点:

  • 类组件中的方法,如果不是箭头函数,都会被编译成function声明的函数
  • setState的底层机制
    • setState自带partialState能力(部分更新)
    • v18:不管写在哪里,都会在下一个任务队列中执行(批处理)
    • v18以前:在同步任务中,会做一次批处理(异步操作);在异步任务中,则跳过批处理(同步操作)

1.关于this问题

先来看一段代码

babel编译前的class,可以自己打开babel官网查看编译结果

class Component{
	tick(){};
  	tick1 = ()=>{};
}
const c = new Component()

babel编译后的class,耐心看注释!!

// 通过这个函数,对目标对象进行劫持
function _defineProperty(obj, key, value) {
  // 获取一个有效的key
  key = _toPropertyKey(key);
  // 再进行遍历添加属性!
  if (key in obj) {
    Object.defineProperty(obj, key, {
      value: value,
      enumerable: true,
      configurable: true,
      writable: true,
    });
  } else {
    obj[key] = value;
  }
  return obj;
}
function _toPropertyKey(arg) {
  var key = _toPrimitive(arg, "string");
  // symbol也能作为key,其他必须通过类型转换,变为string类型!
  return typeof key === "symbol" ? key : String(key);
}
/* 
  这个函数是为了:获取一个有效的key
  + 因为key有可能不是string(例如symbol),但是对象成员访问时,会把key隐式转换为string类型!
  + 在转换的过程中,需要看这个key有没有实现Symbol.toPrimitive方法!
  + 因为隐式转换最先找的,就是Symbol.toPrimitive这个方法!
*/
function _toPrimitive(input, hint) {
  if (typeof input !== "object" || input === null) return input;
  var prim = input[Symbol.toPrimitive];
  if (prim !== undefined) {
    var res = prim.call(input, hint || "default");
    if (typeof res !== "object") return res;
    throw new TypeError("@@toPrimitive must return a primitive value.");
  }
  return (hint === "string" ? String : Number)(input);
}
class Component {
  constructor() {
    // 关键在这里:tick1 永远绑定在this上,this就是类的实例,所以通过this一定能拿到tick1
    _defineProperty(this, "tick1", () => {});
  }
  /* 
    这样写最后会作为Component类的原型方法,且是function类型的函数!
    而function是谁调用指向谁!所以这种函数,都需要加bind!!!
  */
  tick() {}
}
const c = new Component();

接着再看一下控制台输出:

手写React(三)实现setState及类组件更新

结论:

  • 对象简写方式的函数,是属于class自身的,放在类的原型上!
  • 箭头函数,会通过defineProperty绑定到实例身上!!
  • 所以箭头函数的this,始终都是实例!!

2.关于setState

1) partialState

类组件中的setState自带partialState能力,也就是局部更新

也就是说:你更新的数据可以只传需要更新的,而不用传全部;[useState不具备这个能力,需要自己封装]

class ClassComponent extends React.Component {
  state = { date: new Date(), num: 1 };

  addNum = () => {
    // 这里只更新了num,底层会帮我们做 {...oldState,...newState} 这样的处理!
    this.setState({ num: this.state.num + 1 });
  };

  render() {
    return (
        <div>
          num: {this.state.num}
          <button onClick={this.addNum}>+</button>
        </div>
    );
  }
}

2) 批处理

先提一个问题:如果你在某个代码块中,调用了一万次setState,那么它需要执行一万次吗?需要更新一万次视图吗?这样合理吗?

上面问题的答案很明显,执行一万次是不合理的!那么我们如何能避免这个问题呢?最佳实践就是批处理。

我们把上面代码中的addNum改成下面这样:

state = { date: new Date(), num: 1 };
addNum = () => {
    // this.setState({ num: this.state.num + 1 });
    for (let i = 0; i < 20; i++) {
      this.setState({ num: this.state.num + 1 });
    }
};

请问render执行多少次? num最终是多少?答案是1次,num是2

3) 实现一个批处理

let queue = [];
let callbacks = []
let isBatchingUpdate = true;
let state = { number: 0 };

// 遍历执行队列
function batchUpdate () {
  Promise.resolve().then(() => {
    queue.forEach(newSate => {
      state = { ...state, ...newSate }
    })
    callbacks.forEach(cb => cb(state))
    queue.length = 0
    isBatchingUpdate = true
  })
}

// 把任务丢到队列
function setState (newSate, callback) {
  queue.push(newSate);
  if (typeof callback === 'function') {
    callbacks.push(callback)
  }
  if (isBatchingUpdate) {
    isBatchingUpdate = false
    batchUpdate()
  }
}

// 执行一百次setState
for (let i = 0; i < 100; i++) {
  setState({ number: state.number + 1 });
}
console.log(state); // 无法拿到最新的

setTimeout(() => {
  console.log(state); // 1
})

setState({ number: state.number + 1 }, (state) => {
  console.log('最新的state', state) // 回调函数中拿到最新值
});

setTimeout(() => {
  for (let i = 0; i < 100; i++) {
    setState({ number: state.number + 1 });
  }
  console.log(state) // 仍然无法拿到最新值!
})

4) 关于传递函数和回调函数

setState可以传递函数,函数中的返回值作为新的state;

我们把上面的setNum改成这样:

state = { date: new Date(), num: 1 };
addNum = () => {
    for (let i = 0; i < 20; i++) {
      this.setState((oldState) => ({ num: oldState.num + 1 }));
    }
};

最终输入结果是21,而render还是只触发一次!

原理:函数中产生了一个新的闭包,oleState会被缓存起来,虽然同样批处理执行了20次,但是每一次的oldState都是上一次改变过的!

setState接收的第二个参数是一个函数,会在数据更新之后执行这个函数[类似vue的nextTick],这里不再赘述

二、实现state/setState

回到正文:在前面的基础上,开始实现Component类的state和setState

1.完善Component

export class Component {
  // 标记它是否是一个类组件,源码中:Component.prototype.isReactComponent = {}
  static isReactComponent = true;
  constructor(props) {
    this.props = props; // 初始化props
    this.state = {}; // 初始化state
    this.updater = new Updater(this); // 初始化组件的更新器
  }

  /* 实现setState方法,将接收的参数,传递给updater.addState */
  setState(partialState, callback) {
    this.updater.addState(partialState, callback);
  }
}

2.Updater

更新操作其实是放在updater中去处理的,现在开始实现这个构造函数

1) 创建Update

class Updater {
  constructor(classInstance) {
    // 初始化类组件实例,每个updater对应一个类组件
    this.classInstance = classInstance;
    // 初始化setState更新队列
    this.pendingState = [];
    // 初始化setState的回调函数队列
    this.callbacks = [];
  }
}

2) addState

将更新操作放入队列中,然后调用emitUpdate方法处理更新逻辑

class Updater {
  // ……

  /* 实现addState方法 */
  addState(partialState, callback) {
    // 将setState放入队列中
    this.pendingState.push(partialState);
    if (typeof callback === "function") {
      // 如果第二个参数是函数,继续放入回调函数队列中
      this.callbacks.push(callback);
    }
    // 执行更新操作
    this.emitUpdate();
  }
  
  // ……
}

3) emitUpdate

class Updater {
  // ……

  /* 不管状态和属性的变化 都会让组件刷新,不管状态变化和属性变化 都会执行此方法 */
  emitUpdate() {
    // 这里后面会有一些更新判断,先跳过
    this.updateComponent(); // 让组件更新
  }

  // ……
}

4) updateComponent

处理组件更新,更新前需要先调用getState方法,获取最新的state状态

class Updater {
  // ……

  /* 处理组件更新 */
  updateComponent() {
    const { classInstance, pendingState } = this;
    // 如果当前更新队列中有数据才执行
    if (pendingState.length > 0) {
      // 将组件实例,和最新的状态,传递给shouldUpdate函数,开始组件更新
      shouldUpdate(classInstance, this.getState());
    }
  }

  // ……
}

5) getState

执行传入的setState,计算出最新的state

class Updater {
  // ……

  /* 根据老状态,和pendingStates这个更新队列,计算新状态 */
  getState() {
    let { classInstance, pendingState } = this;
    let { state } = classInstance; // 取出组件实例上的state
    // 遍历执行队列
    pendingState.forEach((nextState) => {
      // 如果传递的是函数,就执行该函数,并把state传递过去
      if (typeof nextState === "function") {
        nextState = nextState(state);
      }
      // 最终合并state
      state = { ...state, ...nextState };
    });
    pendingState.length = 0; // 清空队列
    classInstance.state = state; // 重新赋值
    this.callbacks.forEach((callback) => callback()); // 执行回调队列
    this.callbacks.length = 0; // 清空回调队列
    return state; // 返回新的状态
  }
  
  // ……
}

6) shouldUpdate

后面这个函数中,会处理是否更新组件的逻辑

/* 这个函数中处理是否更新组件的逻辑 */
function shouldUpdate(classInstance, nextState) {
  console.log('状态修改完毕,开始更新组件')
}

7) 事件处理

回到react-dom中,找到updateProps方法,需要对传入的onXxx事件做一下简单处理

function updateProps(dom, oldProps, newProps) {
  for (let key in newProps) {
    // …… 如果传入的属性 是以on开头 就进行事件绑定
    } else if (key.startsWith("on")) {
      // 先简单处理下事件绑定,后面再改为合成事件!
      dom[key.toLocaleLowerCase()] = newProps[key]; //dom.onclick=handleClick
    } else {
    // ……
  }
}

8) 查看效果

回到index.jsx中,创建一个案例,点击按钮改变state,测试一下

// 用自己封装的
import React from "./my-react/react";
import ReactDOM from "./my-react/react-dom";

class ClassComponent extends React.Component {
  state = { date: new Date(), num: 1 };

  addNum = () => {
    this.setState({ num: this.state.num + 1 }, () => {
      console.log(this.state.num);
    });
  };

  render() {
    return (
      <div id="abc">
        num: {this.state.num}
        <button onClick={this.addNum}>+</button>
      </div>
    );
  }
}

let element = React.createElement(ClassComponent);

/* 
  注意: v18八本的 jsx编译后的vdom是被冻结的!!
  + 被冻结的原因:为了安全,以及规范
  + 就像你进学校,只能通过校门进去,不能翻墙进去!
  + 所以,如果你是用的v18版本,那么需要降低版本到v17!!
*/

ReactDOM.render(element, document.getElementById("root"));

可以看到,回调函数中打印的state已经发生改变,这一步已经简单实现了!后面再来完善批处理!

手写React(三)实现setState及类组件更新

三、实现组件更新

我们先对上面的shouldUpdate方法进行完善

1.完善shouldUpdate

/* 这个函数中处理是否更新组件的逻辑 */
function shouldUpdate(classInstance, nextState) {
  // 将实例上的state,改为最新的state,这里后面再做比对优化……
  classInstance.state = nextState;
  // 调用组件实例的forceUpdate方法,更新组件
  classInstance.forceUpdate();
}

2.完善真实dom查找

上面可以看到,我们接着要实现forceUpdate方法,但是在实现此方法之前,还要做一些事:

  • 在每个vdom上,都添加一个dom属性,存放它的真实dom!
  • 组件在作为vdom的type时,并没有真实dom,需要添加oldRenderVdom属性,以便于查找其真实dom

1) 完善createDom

回到react-dom.js中,在每次真实dom创建完后,都将真实dom赋值给虚拟dom的dom属性!

function createDom(vdom) {
  // ……
  /* 
    将真实dom挂载再vdom身上,
    面组件更新时,子节点需要用将真实dom作为parent,然后才能操作dom更新
    后面如果设置了ref 还会把这个dom给ref
  */
  vdom.dom = dom;
  return dom;
}

2) 完善mountClassComponent

function mountClassComponent(vdom) {
  let { type, props } = vdom;
  // new 类组件构造函数 得到实例
  let classInstance = new type(props);
  // 执行实例上面的render方法,拿到vdom
  let renderVdom = classInstance.render();
  
  /* ------------------------------------------------- */
  // 将虚拟dom挂载到实例上,这样后面才能通过这个属性 找到对应的真实dom!
  classInstance.oldRenderVdom = vdom.oldRenderVdom = renderVdom;
  /* ------------------------------------------------- */
  
  // 递归创建真实dom
  return createDom(renderVdom);
}

3 实现findDOM

用于递归查找当前vdom的真实dom,我们将新的真实dom替换旧的真实dom的时候,肯定需要找到之前的真实dom才能替换!!

到react-dom.js中,创建此方法

/* 
  查找vdom身上的真实dom!
  这个真实dom,就是更新时的容器!
  只有找到这个容器,我们才知道新的元素因该放在哪里!!!
*/
export function findDOM(vdom) {
  let { type, oldRenderVdom } = vdom;
  let dom;
  if (isBaseType(type) || type === REACT_TEXT) {
    // 原生标签
    dom = vdom.dom; // 这个dom 在createDom时创建的!
  } else if (oldRenderVdom) {
    /* 
      可能是函数组件/类组件,需要递归调用,因为组件 作为vdom的时候,还没有真实dom,
      组件需要被调用一次, 再递归调用createDom的时候,才有真实dom!
      并且 组件 可能会嵌套!!例如:
      
      const Child = () => <div>111</div>;
      const Parent = () => <Child />;
      
      此时Parent身上没有真实dom! 它的dom还是一个vdom!所以需要递归继续找!
    */
    dom = findDOM(oldRenderVdom);
  }
  return dom;
}

4.实现forceUpdate

回到Component中,我们开始来实现组件的强制更新函数

export class Component {
  // ……
  /* 
    强制更新一次组件,组件更新:
    + 1.获取 老的虚拟DOM <React元素>
    + 2.根据最新的属生和状态计算新的虚拟DOM
    + 然后进行比较,查找差异,然后把这些差异同步到真实DOM上
  */
  forceUpdate() {
    // 获取到老的虚拟dom
    let oldRenderVdom = this.oldRenderVdom;
    // 找到老的真实dom
    let oldDOM = findDOM(oldRenderVdom);
    // 在调用forceUpdate之前,state已经更新完毕,此时拿到新的虚拟dom
    let newRenderVdom = this.render();
    // 更新视图: 比对新旧虚拟dom,创建出新的真实dom, 插入到旧的真实dom上
    compareTwoVdom(oldDOM, oldRenderVdom, newRenderVdom);
    // 更新完后,将新的虚拟dom作为下一次更新的旧的虚拟dom
    this.oldRenderVdom = newRenderVdom;
  }
  // ……
}

5.实现compareTwoVdom

在这里有一个diff算法 比对新旧虚拟dom,我们后面再一步步实现! 先暴力更新处理一次!

  1. 根据传入的旧的虚拟dom,找到旧的真实dom
  2. 根据传入的新的虚拟dom,创建新的真实dom
  3. 将新的真实dom,替换调旧的真实dom
/**
 * @description    : 比较新旧虚拟dom,找出差异,更新真实dom
 * @param           { } parentDom
 * @param           { } oldVdom
 * @param           { } newVdom
 * @return          { }
 */
export function compareTwoVdom(parentDom, oldVdom, newVdom) {
  // 找到原来的真实dom
  let oldDom = findDOM(oldVdom);
  // 创建出新的真实dom
  let newDom = createDom(newVdom);
  // 用新的 替换旧的!后面再做diff  做节点比对。。。
  parentDom.parentNode.replaceChild(newDom, oldDom);
  console.log('组件更新完毕!')
}

6.看下效果

点击 + 按钮,视图实现更新!

手写React(三)实现setState及类组件更新

7.完整代码如下

index.jsx

// import React from "react";
// import ReactDOM from "react-dom/client";

// 用自己封装的
import React from "./my-react/react";
import ReactDOM from "./my-react/react-dom";

class ClassComponent extends React.Component {
  state = { date: new Date(), num: 1 };

  addNum = () => {
    this.setState({ num: this.state.num + 1 }, () => {
      console.log("最新的state", this.state.num);
    });
  };

  render() {
    return (
      <div id="abc">
        num: {this.state.num}
        <button onClick={this.addNum}>+</button>
      </div>
    );
  }
}

let element = React.createElement(ClassComponent);

/* 
  注意: v18八本的 jsx编译后的vdom是被冻结的!!
  + 被冻结的原因:为了安全,以及规范
  + 就像你进学校,只能通过校门进去,不能翻墙进去!
  + 所以,如果你是用的v18版本,那么需要降低版本到v17!!
*/

ReactDOM.render(element, document.getElementById("root"));

Component.js

/*
 * @Description  : 类组件实现
 * @Author       : zhangyuru
 * @FilePath     : Component.js
 */

import { compareTwoVdom, findDOM } from "./react-dom";

/**
 * @description    : Component父类
 * @return          { void }
 */
export class Component {
  // 标记它是否是一个类组件,源码中:Component.prototype.isReactComponent = {}
  static isReactComponent = true;
  constructor(props) {
    this.props = props; // 初始化props
    this.state = {}; // 初始化state
    this.updater = new Updater(this); // 初始化组件的更新器
  }

  /* 实现setState方法,将接收的参数,传递给updater.addState */
  setState(partialState, callback) {
    this.updater.addState(partialState, callback);
  }

  /* 
    强制更新一次组件,组件更新:
    + 1.获取 老的虚拟DOM <React元素>
    + 2.根据最新的属生和状态计算新的虚拟DOM
    + 然后进行比较,查找差异,然后把这些差异同步到真实DOM上
  */
  forceUpdate() {
    // 老的虚拟dom
    let oldRenderVdom = this.oldRenderVdom;
    // 找到老的真实dom
    let oldDOM = findDOM(oldRenderVdom);
    // 在调用forceUpdate之前,state已经更新完毕,此时拿到新的虚拟dom
    let newRenderVdom = this.render();
    // 更新视图: 比对新旧虚拟dom,创建出新的真实dom, 插入到旧的真实dom上
    compareTwoVdom(oldDOM, oldRenderVdom, newRenderVdom);
    // 更新完后,将新的虚拟dom作为下一次更新的旧的虚拟dom
    this.oldRenderVdom = newRenderVdom;
  }
}

class Updater {
  constructor(classInstance) {
    // 初始化类组件实例,每个updater对应一个类组件
    this.classInstance = classInstance;
    // 初始化setState更新队列
    this.pendingState = [];
    // 初始化setState的回调函数队列
    this.callbacks = [];
  }

  /* 实现addState方法 */
  addState(partialState, callback) {
    // 将setState放入队列中
    this.pendingState.push(partialState);
    if (typeof callback === "function") {
      // 如果第二个参数是函数,继续放入回调函数队列中
      this.callbacks.push(callback);
    }
    // 执行更新操作
    this.emitUpdate();
  }

  /* 不管状态和属性的变化 都会让组件刷新,不管状态变化和属性变化 都会执行此方法 */
  emitUpdate() {
    // 这里后面会有一些更新判断,先跳过
    this.updateComponent(); // 让组件更新
  }

  /* 处理组件更新 */
  updateComponent() {
    const { classInstance, pendingState } = this;
    // 如果当前更新队列中有数据才执行
    if (pendingState.length > 0) {
      // 将组件实例,和最新的状态,传递给shouldUpdate函数,开始组件更新
      shouldUpdate(classInstance, this.getState());
    }
  }

  /* 根据老状态,和pendingStates这个更新队列,计算新状态 */
  getState() {
    let { classInstance, pendingState } = this;
    let { state } = classInstance; // 取出组件实例上的state
    // 遍历执行队列
    pendingState.forEach((nextState) => {
      // 如果传递的是函数,就执行该函数,并把state传递过去
      if (typeof nextState === "function") {
        nextState = nextState(state);
      }
      // 最终合并state
      state = { ...state, ...nextState };
    });
    pendingState.length = 0; // 清空队列
    this.callbacks.forEach((callback) => callback()); // 执行回调队列
    this.callbacks.length = 0; // 清空回调队列
    return state; // 返回新的状态
  }
}

/* 这个函数中处理是否更新组件的逻辑 */
function shouldUpdate(classInstance, nextState) {
  classInstance.state = nextState;
  classInstance.forceUpdate();
}

constans.js

/*
 * @Description  : 手写React的常量存放
 * @Author       : zhangyuru
 * @FilePath     : contants.js
 */

/* 表示这是一个文本类型的元素 在源码里没有这样一个类型 */
export const REACT_TEXT = Symbol("REACT_TEXT");

react-dom.js

/*
 * @Description  : 手写react-dom
 * @Author       : zhangyuru
 * @FilePath     : react-dom.js
 */
import { REACT_TEXT } from "./contants";
import { isBaseType } from "./utils";

/**
 * @description    : render方法,创建真实dom,插入到指定容器中
 * @param           { } vdom
 * @param           { } container
 * @return          { } void
 */
function render(vdom, container) {
  let newVdom = createDom(vdom); // 调用此方法创建真实dom
  container.appendChild(newVdom); // 将真实dom插入到容器中
}

/**
 * @description    : 根据vdom的描述 创建真实dom
 * @param           { } vdom
 * @return          { } 真实dom元素
 */
function createDom(vdom) {
  let { type, props } = vdom;
  let dom;

  if (type === REACT_TEXT) {
    // 类型是文本节点,直接创建文本
    dom = document.createTextNode(props.content);
  } else if (typeof type === "function") {
    // 可能是类组件,也可能是函数组件
    if (type.isReactComponent) {
      // 有isReactComponent属性 说明它是一个类组件
      return mountClassComponent(vdom);
    } else {
      // 否则就是普通函数组件
      return mountFunctionComponent(vdom);
    }
  } else if (typeof type === "string") {
    // 类型是string,说明它是一个标签名,创建对应元素
    dom = document.createElement(type);
  } else if (isBaseType(vdom)) {
    // 新版本编译出来的vdom可能是一个string或者number类型,当作文本处理就好
    dom = document.createTextNode(vdom);
  } else {
    throw new Error("无法处理元素类型", type);
  }

  if (props) {
    // 最props进行处理,根据vdom的描述,挂载/更新元素的属性
    updateProps(dom, {}, props);

    // 对children进行处理
    if (typeof props?.children === "object" && !!props?.children?.type) {
      // 如果children是对象[只有一个儿子],递归调render继续创建
      render(props.children, dom);
    }
    if (Array.isArray(props?.children)) {
      // 如果children是数组 需要遍历再递归创建
      reconcileChidren(props.children, dom);
    }
    if (isBaseType(props.children)) {
      // 新版本编译出来的children有可能是string或者number,也当作文本处理
      render(props.children, dom);
    }
  }
  /* 
    将真实dom挂载再vdom身上,
    面组件更新时,子节点需要用将真实dom作为parent,然后才能操作dom更新
    后面如果设置了ref 还会把这个dom给ref
  */
  vdom.dom = dom;
  return dom;
}

/**
 * @description    : 挂载类组件
 * @param           { } vdom
 * @return          { } vdom
 */
function mountClassComponent(vdom) {
  let { type, props } = vdom;
  // new 类组件构造函数 得到实例
  let classInstance = new type(props);
  // 执行实例上面的render方法,拿到vdom
  let renderVdom = classInstance.render();

  /* ------------------------------------------------- */
  // 将虚拟dom挂载到实例上,这样后面才能通过这个属性 找到对应的真实dom!
  classInstance.oldRenderVdom = vdom.oldRenderVdom = renderVdom;
  /* ------------------------------------------------- */
  
  // 递归创建真实dom
  return createDom(renderVdom);
}

/**
 * @description    : 处理函数组件
 * @param           { } vdom
 * @return          { } vdom
 */
function mountFunctionComponent(vdom) {
  const { type, props } = vdom;
  // type类型是函数,就调用该函数,将props传过去
  let renderVdom = type(props);
  // 将函数返回的vdom,继续递归创建真实dom
  return createDom(renderVdom);
}

/**
 * @description    : 循环创建/更新子节点
 * @param           { } children
 * @param           { } parentDom
 * @return          { } void
 */
function reconcileChidren(children, parentDom) {
  for (let i = 0; i < children.length; i++) {
    let childVdom = children[i];
    render(childVdom, parentDom); // 递归render
  }
}

/**
 * @description    : 根据vdom的描述,挂载/更新元素的属性
 * @param           { } dom
 * @param           { } oldProps 暂时没用,后面会用作diff
 * @param           { } newProps 新的props
 * @return          { } void
 */
function updateProps(dom, oldProps, newProps) {
  for (let key in newProps) {
    if (key === "children") {
      continue; // 暂时跳过,后面会单独处理子节点
    }
    if (key === "style") {
      let styleObj = newProps[key];
      for (let attr in styleObj) {
        dom.style[attr] = styleObj[attr];
      }
    } else if (key.startsWith("on")) {
      // 先简单处理下事件绑定,后面再改为合成事件!
      dom[key.toLocaleLowerCase()] = newProps[key]; //dom.onclick=handleClick
    } else {
      if (newProps[key]) dom[key] = newProps[key];
    }
  }
}

/* 
  查找vdom身上的真实dom!
  这个真实dom,就是更新时的容器!
  只有找到这个容器,我们才知道新的元素因该放在哪里!!!
*/
export function findDOM(vdom) {
  let { type, oldRenderVdom } = vdom;
  let dom;
  if (isBaseType(type) || type === REACT_TEXT) {
    // 原生标签
    dom = vdom.dom; // 这个dom 在createDom时创建的!
  } else if (oldRenderVdom) {
    /* 
      可能是函数组件/类组件,需要递归调用,因为组件 作为vdom的时候,还没有真实dom,
      组件需要被调用一次, 再递归调用createDom的时候,才有真实dom!
      并且 组件 可能会嵌套!!例如:
      const Child = () => <div>111</div>;
      const Parent = () => <Child />;
      此时Parent身上没有真实dom! 它的dom还是一个vdom!所以需要递归继续找!
    */
    dom = findDOM(oldRenderVdom);
  }
  return dom;
}

/**
 * @description    : 比较新旧虚拟dom,找出差异,更新真实dom
 * @param           { } parentDom
 * @param           { } oldVdom
 * @param           { } newVdom
 * @return          { }
 */
export function compareTwoVdom(parentDom, oldVdom, newVdom) {
  // 找到原来的真实dom
  let oldDom = findDOM(oldVdom);
  // 创建出新的真实dom
  let newDom = createDom(newVdom);
  // 用新的 替换旧的!后面再做diff  做节点比对。。。
  parentDom.parentNode.replaceChild(newDom, oldDom);
  console.log('组件更新完毕!')
}

const ReactDOM = {
  render,
};

export default ReactDOM;

react.js

/*
 * @Description  : 手写react
 * @Author       : zhangyuru
 * @FilePath     : react.js
 */

import { wrapToVdom } from "./utils";
import { Component } from "./Component";

/**
 * @description    : 实现createElement方法
 * @param           { } type 元素类型
 * @param           { } config 元素属性
 * @param           { } children 子元素
 * @return          { } vdom
 */
function createElement(type, config, children) {
  if (config) {
    delete config.__source;
    delete config.__self;
    delete config.ref;
    delete config.key;
  }
  // 将接受的第二个参数转为props
  let props = { ...(config || {}) };
  // 对children的处理
  if (arguments.length > 3) {
    // 如果参数个数大于3个,说明不止一个子节点,截取子节点并用wrapToVdom处理
    props.children = Array.prototype.slice.call(arguments, 2).map(wrapToVdom);
    // props.children = Array.prototype.slice.call(arguments, 2);
  } else {
    // 只有一个子节点的情况下,继续用wrapToVdom处理
    props.children = wrapToVdom(children);
  }
  // 返回值后面会继续扩展
  return { type, props };
}

const React = {
  createElement,
  Component,
};

export default React;

utils.js

/*
 * @Description  : 手写React的工具函数
 * @Author       : zhangyuru
 * @FilePath     : utils.js
 */

import { REACT_TEXT } from "./contants";

/**
 * @description    : 创建vdom对象 标记子节点类型
 * @param           { } element
 * @return          { } vdom
 */
export function wrapToVdom(element) {
  if (typeof element === "string" || typeof element === "number") {
    // 虚拟DOM.props.content就是此元素的内容
    return { type: REACT_TEXT, props: { content: element } };
  } else {
    return element;
  }
}

/* ---------------------utils中的辅助函数----------------------- */

/**
 * @description    : 判断是不是能被有效渲染的初始值
 * @param           { any } target
 * @return          { boolean } boolean
 */
export function isBaseType(target) {
  return typeof target === "string" || typeof target === "number";
}
转载自:https://juejin.cn/post/7274163003157725184
评论
请登录