React15 核心原理模拟实现

本文介绍了如何模拟实现React15的核心功能,包括搭建开发环境,创建并导出`createElement`方法,实现`render`方法,处理普通节点和组件的初次渲染,以及渲染更新和组件状态更新的逻辑。通过自定义的`React`,实现了jsx代码转换、虚拟DOM的创建、DOM的更新和渲染。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

搭建开发环境

要模拟 React 的核心原理我们只需要将babel转换jsx代码是调用的createElement方法转换成自定义的React即可。所以搭建开发环境时只需要能够转换jsx和es语法以及能够启动开发服务器即可。

创建文件结构
在空文件夹下创建如下的文件结构
在这里插入图片描述
其中TinyReact文件夹存放要实现的React核心代码

开发依赖

依赖插件描述
webpack打包工具
webpack-cliwebpack命令行工具
webpack-dev-server开发服务器插件,用于启动一个开发服务器
html-webpack-plugin根据模板生成html文件,并自定引入打包好的js、css等文件
clean-webpack-plugin打包之前清除文件
babel-loaderjs文件加载器,用于转换文件中的jsx以及es6语法
@babel/corebabel核心库
@babel/preset-env将es6语法转换成es5
@babel/preset-react将jsx语法转化成普通的es5代码

webpack配置

// webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = {
	entry: './src/index.js',
	mode: 'development',
	output: {
		filename: 'bundle.js',
		path: path.resolve(__dirname, 'dist')
	},
	devtool: 'source-map',
	devServer: {
		hot: true
	},
	module: {
		rules: [
			{
				test: /\.js$/,
				exclude: /node_modules/,
				use: 'babel-loader'
			}
		]
	},
	plugins: [
		new CleanWebpackPlugin(),
		new HtmlWebpackPlugin({
			template: './index.html'
		})
	]
}

babel配置
要想让babel使用自定义的React提供的方法编译jsx有两种方式:

  1. 注释:在要使用自定义React转换的文件开头使用注释/** @jsx TinyReact.createElement */来告诉babel要使用指定的方法来转换当前文件的jsx语法。
    在这里插入图片描述
  2. 修改babel配置:给转换jsx语法的插件@babel/preset-react插件的选项参数gragma传入自定义的createElement方法
    .babelrc文件
    {
        "presets": [
            "@babel/preset-env",
            [
                "@babel/preset-react",
                {
                    "pragma": "TinyReact.createElement"
                }
            ]
        ]
    }
    

配置命令行启动

"scripts": {
    "start": "webpack serve"
  },

模拟实现React15核心功能

首先在TinyReact文件夹中创建index.js用于集中导出,要保持index.js文件的纯净,只做导出。

转换jsx代码
在使用babel转换jsx代码时,会调用React提供的createElement方法。因此首先要创建一个createElement.js文件并导出一个createElement方法。然后在TinyReact/index.js中导出。这里这样导出的原因是createElement的使用是通过TinyReact.createElement这种方式来使用的。

// TinyReact/index.js
import createElement from "./createElement"

export default {
  createElement
}

createElement(type, config, …children)接收3个以上的参数,返回一个virtualDom对象。

type: 节点类型
config: 用户传入的参数对象
children:从第三个参数开始都是节点的子节点

/**
 * 创建virtualDom对象
 * 1. 排除为false/true/null的子节点,这些节点不会渲染
 * 2. 处理子节点中的文本节点,将文本节点转化为对象
 * 3. 将处理后的子节点数组添加到返回对象的props属性,并合并选项参数config
 * 4. 返回virtualDom对象
 */
export default function createElement(type, config, ...children) {
  const childElements = [].concat(...children).reduce((r, child) => {
    if (child !== false || child !== true || child !== null) {
      if (child instanceof Object) {
        r.push(child)
      } else { // 文本节点
        r.push(createElement('text', {textContent: child}))
      }
    }
    return r
  }, [])
  return {
    type,
    props: Object.assign({children: childElements}, config)
  }
}

文本节点传入时是字符串形式,为了方便后续处理,需要将字符串转化为统一的virtualDom对象。

在src/index.js编写代码测试,输出jsx转换之后的结果。

// src/index.js
import TinyReact from "./TinyReact"

const jsx = (
	<div>
		<p>哈哈112</p>
		<button onClick={() => { alert(123) }}>按钮</button>
	</div>
)
console.log(jsx)

yarn start启动项目之后观察浏览器后台输出确实是经过自定义createElement转换之后的结果。
在这里插入图片描述
render方法的实现

render方法主要功能是将virtualDom转化为真实Dom,并渲染到容器中。这里我们先不考虑组件,只考虑html标签的渲染。

普通节点初次渲染
在TinyReact文件夹中创建一个render文件夹,并导出一个render方法,render方法暂时只接受两个参数:virtualDom是要渲染的虚拟Dom,container是一个真实的dom,是virtualDom渲染的容器。在render方法中调用diff方法。在diff方法中统一处理dom的更新和渲染。

// TinyReact/render.js
import diff from "./diff";

export default function render(virtualDom, container) {
  diff(virtualDom, container)
}
// TinyReact/index.js
import createElement from "./createElement"
export { default as render } from "./render"

export default {
  createElement
}

TinyReact下创建diff.js文件,导出diff方法。diff方法要判断是否是初次渲染,如果是初次渲染,需要调用mountElement方法来渲染virtualDom。更新的情况我们稍后处理。

// TinyReact/diff.js
import mountElement from "./mountElement"

export default function diff(virtualDom, container, oldDom) {
  const oldVirtualDom = oldDom && oldDom.__virtualDom
  if (!oldVirtualDom) { // 是否已经渲染,通过判断老节点是否存在
    // 初次渲染
    mountElement(virtualDom, container)
  }
}

再次在TinyReact下创建mountElement.js文件导出mountElement方法。该方法主要功能是将根据virtualDom来创建真实Dom,然后将真实Dom渲染到容器中。

这里我们要根据virtualDom的类型来分别。virtualDom可能是一个普通节点,也可能是一个组件。作为一个普通节点时virtualDom的type属性一定是一个字符串,这里我们将一些判断性的功能函数都放到utils.js文件中管理。组件时type可能是一个类或函数。要为这两种情况分别定义两个方法来处理。
处理普通节点时我们使用mountNativeElement方法来处理

import mountNativeElement from "./mountNativeElement";
import { isComponent } from "./utils";

/**
 * 渲染虚拟dom
 * @param {*} virtualDom 
 * @param {*} container 
 */
export default function mountElement(virtualDom, container, oldDom) {
  // 判断虚拟dom类型是组件还是普通元素
  if (isComponent(virtualDom)) { // 组件
    
  } else { // 普通元素
    mountNativeElement(virtualDom, container)
  }
}
// TinyReact/utils.js
/**
 * 判断节点是否是组件节点
 * @param {*} virtualDom 
 * @returns 
 */
export const isComponent = virtualDom => {
  return typeof virtualDom.type !== 'string'
}

TinyReact下创建mountNativeElement.js文件导出mountNativeElement方法。主要功能是根据virtualDom对象创建真实Dom,然后渲染到container容器中。这里创建dom对象的过程可以封装到一个单独的方法createDomElement中进行管理。

// TinyReact/mountNativeElement.js
import createDomElement from "./createDomElement"
/**
 * 渲染普通元素节点
 * @param {*} virtualDom 
 * @param {*} container 
 */
export default function mountNativeElement(virtualDom, container) {
  // 判断是文本节点还是元素节点
  if (virtualDom.type === 'text') { // 文本节点,container文本内容设置为文本节点内容
    container.textContent = virtualDom.props.textContent
  } else { // 元素节点,创建节点,并设置节点属性,将节点添加到container
    // 创建节点
    const element = createDomElement(virtualDom)

    // 将节点添加到container中
    container.appendChild(element)
  }
}

createDomElement创建dom时,需要区分是文本节点还是元素节点,这两种节点的创建方法有所不同。

如果是文本节点,只需要创建并返回节点dom即可。

如果是元素节点,需要将props中的一些属性设置为元素的属性,并为元素添加事件处理。此外还要对节点的子节点进行递归处理。节点属性的处理功能能被复用所以创建updateElementNode.js文件来定义一个updateElementNode方法来处理。

// TinyReact/createDomElement.js
import mountElement from "./mountElement"
import updateElementNode from "./updateElementNode"

/**
 * 根据虚拟Dom创建真实Dom
 * @param {*} virtualDom 
 * @returns 
 */
export default function createDomElement(virtualDom) {
  let element = null
  // 判断是文本节点还是元素节点
  if (virtualDom.type === 'text') { // 文本节点,container文本内容设置为文本节点内容
    element = document.createTextNode(virtualDom.props.textContent)
  } else { // 元素节点,创建节点,并设置节点属性,将节点添加到container
    // 创建节点
    element = document.createElement(virtualDom.type)

    // 为节点添加/更新属性
    updateElementNode(virtualDom, element)

    // 将virtualDom 对象记录在真实dom上
    element.__virtualDom = virtualDom

    // 遍历处理子节点元素
    virtualDom.props.children.forEach(child => {
      mountElement(child, element)
    })
  }

  return element
}
// TinyRreact/updateElementNode.js
/**
 * 创建/更新节点属性
 * @param {*} element 
 * @param {*} virtualDom 
 */
export default function updateElementNode(element, virtualDom) {
  // 获取节点属性
  const props = virtualDom.props
  // 遍历节点属性,将节点属性赋给dom元素
  Object.keys(props).forEach(propName => {
    // 获取节点属性对应的值
    const propValue = props[propName]
    if (propName.startsWith('on')) { // 以on开头的属性是事件属性,需要为dom添加事件监听
      const eventName = propName.slice(2).toLowerCase()
      element.addEventListener(eventName, propValue)
    } else if (propName === 'value' || propName === 'selected') { // value和selected需要设置到dom上
      element[propName] = propValue
    } else if (propName === 'className') { // 对className进行处理,转化为class
      element.setAttribute('class', propValue)
    } else if (propName !== 'children') { // 为 dom 添加 children 以外的属性
      element.setAttribute(propName, propValue)
    }
  })
}

到此为止,非组件节点的渲染就实现完了。启动项目,我们定义的展示内容就呈现在浏览器上了。

组件的初次渲染
在mountElement方法判断了节点是组件还是普通节点,如果是组件则调用mountComponent方法来渲染。

// TinyReact/mountElement.js
import mountComponent from "./mountComponent";
import mountNativeElement from "./mountNativeElement";
import { isComponent } from "./utils";

/**
 * 渲染虚拟dom
 * @param {*} virtualDom 
 * @param {*} container 
 */
export default function mountElement(virtualDom, container, oldDom) {
  // 判断虚拟dom类型是组件还是普通元素
  if (isComponent(virtualDom)) { // 组件
    mountComponent(virtualDom, container)
  } else { // 普通元素
    mountNativeElement(virtualDom, container)
  }
}

创建mountComponent.js文件,导出mountComponent方法来渲染组件。组件的渲染时,首先要现将组件转化为virtualDom对象,然后再调用mountElement来渲染组件。类组件需要通过执行实例的render方法来获取类组件的virtualDom对象,函数组件则通过直接调用函数本身来获取组件对应的virtualDom对象。

// TinyReact/mountComponent.js
import mountElement from "./mountElement";
import { isFunctionComponent } from "./utils";

/**
 * 渲染组件元素
 * @param {*} virtualDom 
 * @param {*} container 
 */
export default function mountComponent(virtualDom, container) {
  let newVirtualDom = null
  if (isFunctionComponent(virtualDom)) { // 函数组件
    newVirtualDom = buildFunctionComponent(virtualDom)
  } else { // 类组件
    newVirtualDom = buildClassComponent(virtualDom)
  }
  // 挂载组件到容器中
  mountElement(newVirtualDom, container)
}

/**
 * 创建function组件的virtualDom对象
 * @param {*} virtualDom 
 * @returns 
 */
function buildFunctionComponent(virtualDom) {
  return virtualDom.type(virtualDom.props || {})
}

/**
 * 创建class组件的virtualDom对象
 * @param {*} virtualDom 
 * @returns 
 */
function buildClassComponent(virtualDom) {
  const instance = new virtualDom.type(virtualDom.props || {})
  return instance.render()
}

在utils.js中添加判断是否是函数组件的方法isFunctionComponent。

/**
 * 判断节点是否是函数组件
 * @param {*} virtualDom 
 * @returns 
 */
export const isFunctionComponent = virtualDom => {
  return typeof virtualDom.type === 'function' && virtualDom.type.prototype && !virtualDom.type.prototype.render
}

此外要定义类组件,还需要定义一个Component类,类组件需要继承该类。方法需要在TinyReact/index.js中导出。

// TinyReact/index.js
import createElement from "./createElement"
export { default as render } from "./render"
import Component from "./Component"

export default {
  Component,
  createElement
}
// TinyReact/Component.js
export default class Component {
  constructor(props) {
    this.props = props
  }
  render() {}
}

在src/index.js中定义一个组件,然后通过render方法渲染组件。启动项目,组件内容呈现到浏览器中。自此组件初次功能实现完毕。

// src/index.js
import TinyReact, { render } from "./TinyReact"
class Hello extends TinyReact.Component {
	constructor(props) {
		super(props)
	}
	render() {
		return (
			<div>
				<p>&hearts;</p>
				<p>{ this.props.title }</p>
				<p>&hearts;</p>
			</div>
		)
	}
}

render(<Hello title="hello" />, root)

渲染更新
页面初次渲染之后,间隔几秒,再次调用render方法渲染不同的内容,将进入到更新流程。

首先在render方法中引入第三个参数oldDom,表示更新之前的真实Dom,默认值为container容器中的第一个子节点。并将oldDom传入diff

// TinyReact/render.js
import diff from "./diff";

export default function render(virtualDom, container, oldDom = container.firstChild) {
  diff(virtualDom, container, oldDom)
}

接下来修改diff方法,实现更新的主要逻辑。渲染更新时,需要判断是组件更新还是普通节点更新。

// TinyReact/diff.js
import diffComponent from "./diffComponent"
import mountElement from "./mountElement"
import { isComponent } from "./utils"

export default function diff(virtualDom, container, oldDom) {
  const oldVirtualDom = oldDom && oldDom.__virtualDom
  if (!oldVirtualDom) { // 是否已经渲染,通过判断老节点是否存在
    // 初次渲染
    mountElement(virtualDom, container, oldDom)
  } else if(isComponent(virtualDom)) { // 组件更新
    const oldComponent = oldVirtualDom.component
    diffComponent(virtualDom, container, oldDom, oldComponent)
  } else { // 普通节点更新
    diffNativeElement(virtualDom, container, oldVirtualDom, oldDom)
  }
}

调用diffComponent更新组件差异时需要用到更新前组件的实例,在创建组件实例时要将组件实例保存到virtualDom上。修改mountComponent.js中的buildClassComponent,创建类组件实例时将实例保存到virtualDom上,同时修改mountComponent方法,挂载元素之后需要调用类组件的componentDidMount生命周期钩子。

// TinyReact/mountComponent.js
import mountElement from "./mountElement";
import { isComponent, isFunctionComponent } from "./utils";

/**
 * 渲染组件元素
 * @param {*} virtualDom 
 * @param {*} container 
 */
export default function mountComponent(virtualDom, container, oldDom) {
  let newVirtualDom = null
  let component = null


  if (isFunctionComponent(virtualDom)) { // 函数组件
    newVirtualDom = buildFunctionComponent(virtualDom)
  } else { // 类组件
    newVirtualDom = buildClassComponent(virtualDom)
    component = newVirtualDom.component
  }

  // 挂载组件到容器中
  mountElement(newVirtualDom, container, oldDom)
  
  // 如果是类组件需要调用组件的生命周期钩子
  if (component) {
    component.componentDidMount()
  }
}

/**
 * 创建function组件的virtualDom对象
 * @param {*} virtualDom 
 * @returns 
 */
function buildFunctionComponent(virtualDom) {
  return virtualDom.type(virtualDom.props || {})
}

/**
 * 创建class组件的virtualDom对象
 * @param {*} virtualDom 
 * @returns 
 */
function buildClassComponent(virtualDom) {
  const instance = new virtualDom.type(virtualDom.props || {})
  const newVirtualDom = instance.render()
  // 记录组件实例,更新时需要通过virtualDom来获取组件实例
  newVirtualDom.component = instance

  return newVirtualDom
}

组件更新
需要判断是否是同一个组件。如果是同一个组件调用updateComponent方法更新组件。否则直接调用mountElement方法将组件渲染到容器中。

// TinyReact/diffComponent.js
import mountElement from "./mountElement";
import updateComponent from "./updateComponent";

export default function diffComponent(virtualDom, container, oldDom, oldComponent) {
  // 1. 判断组件是否是同一个组件
  if(isSameComponent(virtualDom, oldComponent)) { // 是同一个组件,对比组件差异部分
    updateComponent(virtualDom, container, oldDom, oldComponent)
  } else { // 不是同一个组件,直接调用渲染新的组件
    mountElement(virtualDom, container, oldDom)
  }
}

export const isSameComponent = (virtualDom, oldComponent) => {
  return oldComponent && virtualDom.type === oldComponent.constructor
}

更新组件首先要更新组件实例的props属性,然后调用组件实例的render方法重新生成虚拟Dom对象,再通过diff渲染差异部分。在此过程中调用组件的生命周期钩子。

// TinyReact/updateComponent.js
import diff from "./diff"

/**
 * 更新组件的差异部分,并触发组件的生命周期函数
 * @param {*} virtualDom 
 * @param {*} container 
 * @param {*} oldDom 
 * @param {*} oldComponent 
 */
export default function updateComponent(virtualDom, container, oldDom, oldComponent) {
  oldComponent.componentWillReceiveProps(virtualDom.props)
  if (oldComponent.shouldComponentUpdate(virtualDom.props)) {
    // 更新之前的props
    const prevProps = oldComponent.props
    // 更新组件的props
    oldComponent.updateProps(virtualDom.props)
    // 重新生成组件的虚拟Dom
    const newVirtualDom = oldComponent.render()
    newVirtualDom.component = oldComponent
    // 通过diff方法更新差异
    diff(newVirtualDom, container, oldDom)
    oldComponent.componentDidUpdate(prevProps)
  }
}

这里需要更新组件实例的props属性,因此需要在TinyReact.Component中定义一个updateProps方法来实现。此外还需要初始化组件的生命周期函数。

// TinyReact/Component.js
export default class Component {
  constructor(props) {
    this.props = props
  }
  render() {}
  updateProps(props) {
    this.props = props
  }
  // 生命周期函数
  componentWillMount() {}
  componentDidMount() {}
  componentWillReceiveProps(nextProps) {}
  shouldComponentUpdate(nextProps, nextState) {
    return nextProps != this.props || nextState != this.state
  }
  componentWillUpdate(nextProps, nextState) {}
  componentDidUpdate(prevProps, preState) {}
  componentWillUnmount() {}
}

普通节点更新
首先判断节点类型是否一致,如果节点类型一致还需要判断是文本节点还是元素节点。如果是文本节点,调用updateTextNode更新文本节点内容。如果是元素节点,则首先调用updateElementNode更新节点属性,然后循环递归对比子节点。最后再调用unmountNodez逐个删除多余节点。

节点类型不一致,直接创建新的dom,然后去替换container中的oldDom。

// TinyReact/diffNativeElement.js
import diff from "./diff"
import unmountNode from "./unmountNode"
import updateElementNode from "./updateElementNode"
import updateTextNode from "./updateTextNode"

export default function diffNativeElement(virtualDom, container, oldVirtualDom, oldDom) {
  if (virtualDom.type === oldVirtualDom.type) { // 类型一致,判断是文本节点还是元素节点
    if (virtualDom.type === 'text') {
      updateTextNode(virtualDom, oldVirtualDom, oldDom)
    } else {
      updateElementNode(virtualDom, oldDom, oldVirtualDom)
      const childNodes = oldDom.childNodes

      // 循环递归对比子节点
      virtualDom.props.children.forEach((child, i) => {
        diff(child, oldDom, childNodes[i])
      })

      // 删除多余节点
      if (childNodes.length > virtualDom.props.children.length) {
        let i = childNodes.length
        while(i > virtualDom.props.children.length) {
          unmountNode(childNodes[i - 1])
          i--
        }
      }
    }
  } else { // 类型不一致,根据virtualDom重新生成dom,然后替换oldDom
    const newDom = createDomElement(virtualDom)
    container.replaceChild(newDom, oldDom)
  }
}

文本节点的更新比较简单,只需要当文本内容不一致时,更新oldDom的textContent。

// TinyReact/updateTextNode.js
/**
 * 更新文本节点文本内容
 * @param {*} virtualDom 
 * @param {*} oldVirtualDom 
 * @param {*} oldDom
 */
export default function updateTextNode(virtualDom, oldVirtualDom, oldDom) {
  if (virtualDom.props.textContent !== oldVirtualDom.props.textContent) {
    oldDom.textContent = virtualDom.props.textContent
    // 更新虚拟Dom
    oldDom.__virtualDom = virtualDom
  }
}

元素节点的属性更新,在原来的基础上对事件属性进行特殊处理,如果是属性更新, 需要注销上一次注册的事件处理函数。同时删除多余属性。

// TinyReact/updateElementNode.js
/**
 * 创建/更新节点属性
 * @param {*} element 
 * @param {*} virtualDom 
 */
export default function updateElementNode(virtualDom, element, oldVirtualDom) {
  // 获取节点属性
  const props = virtualDom.props || {}
  const oldProps = (oldVirtualDom && oldVirtualDom.props) || {}

  // 遍历节点属性,将节点属性赋给dom元素
  Object.keys(props).forEach(propName => {
    // 获取节点属性对应的值
    const propValue = props[propName]
    const oldPropValue = oldProps[propName]
    if (propName.startsWith('on')) { // 以on开头的属性是事件属性,需要为dom添加事件监听
      const eventName = propName.slice(2).toLowerCase()
      element.addEventListener(eventName, propValue)
      if (oldVirtualDom) { // 事件属性更新,需要取消上一个事件监听函数
        element.removeEventListener(eventName, oldPropValue)
      }
    } else if (propName === 'value' || propName === 'selected') { // value和selected需要设置到dom上
      element[propName] = propValue
    } else if (propName === 'className') { // 对className进行处理,转化为class
      element.setAttribute('class', propValue)
    } else if (propName !== 'children') { // 为 dom 添加 children 以外的属性
      element.setAttribute(propName, propValue)
    }
  })

  // 遍历oldProps,删除 props中没有的属性
  Object.keys(oldProps).forEach(propName => {
    if (!props[propName]) {
      const oldPropValue = oldProps[propName]
      if (propName.startsWith('on')) { // 以on开头的属性是事件属性,需要为dom添加事件监听
        const eventName = propName.slice(2).toLowerCase()
        // 删除事件属性,需要取消事件监听函数
        element.removeEventListener(eventName, oldPropValue)
      } else if (propName === 'value' || propName === 'selected') { // value和selected需要设置到dom上
        element[propName] = undefined
      } else if (propName === 'className') { // 对className进行处理,转化为class
        element.removeAttribute('class', propValue)
      } else if (propName !== 'children') { // 为 dom 添加 children 以外的属性
        element.removeAttribute(propName, propValue)
      }
    }
  })
}

节点的删除则是通过节点自身的remove方法来实现。

// TinyReact/unmountNode.js
export default function unmountNode(node) {
  node.remove()
}

以上就是渲染更新功能的实现。

component state更新

通过组件实例调用setState更新state,需要在Component.js中添加setState方法。组件state更新之后需要经过以下过程:

  1. 合并state状态。

  2. 调用render方法生成新的virtualDom。

  3. 获取组件state更新之前的真实Dom。
    要获取组件state更新之前的真实Dom,需要在生成组件对应真实Dom时,将生成的真实Dom记录到组件的实例上。

    // TinyReact/createDomElement.js
    import mountElement from "./mountElement"
    import updateElementNode from "./updateElementNode"
    
    /**
     * 根据虚拟Dom创建真实Dom
     * @param {*} virtualDom 
     * @returns 
     */
    export default function createDomElement(virtualDom, oldDom) {
      let element = null
      // 判断是文本节点还是元素节点
      if (virtualDom.type === 'text') { // 文本节点,container文本内容设置为文本节点内容
        element = document.createTextNode(virtualDom.props.textContent)
      } else { // 元素节点,创建节点,并设置节点属性,将节点添加到container
        // 创建节点
        element = document.createElement(virtualDom.type)
    
        // 为节点添加/更新属性
        updateElementNode(virtualDom, element, oldDom)
    
        // 将virtualDom 对象记录在真实dom上
        element.__virtualDom = virtualDom
    
        // 记录组件实例对应的真实Dom
        const component = virtualDom.component
        if (component) {
          component.setDom(element)
        }
    
        // 遍历处理子节点元素
        virtualDom.props.children.forEach((child, i) => {
          mountElement(child, element)
        })
      }
    
      return element
    }
    
  4. 调用diff方法渲染state更新后的差异。

// TinyReact/Component.js
import diff from "./diff"

export default class Component {
  constructor(props) {
    this.props = props
  }
  render() {}
  updateProps(props) {
    this.props = props
  }
  setDom(dom) {
    this._dom = dom
  }
  getDom() {
    return this._dom
  }
  setState(partialState) {
    // 合并状态,
    this.state = Object.assign({}, this.state, partialState)
    // 调用render重新获取virtualDom
    const virtualDom = this.render()
    // 获取组件实例对应的dom
    const oldDom = this.getDom()
    // 调用diff更新差异
    diff(virtualDom, oldDom.parentNode, oldDom)
  }
  // 生命周期函数
  componentWillMount() {}
  componentDidMount() {}
  componentWillReceiveProps(nextProps) {}
  shouldComponentUpdate(nextProps, nextState) {
    return nextProps != this.props || nextState != this.state
  }
  componentWillUpdate(nextProps, nextState) {}
  componentDidUpdate(prevProps, preState) {}
  componentWillUnmount() {}
}

组件更新
组件更新需要区分更新前后是否是同一个组件

  1. 更新前后是同一个组件,需要先将老的组件实例的 props 更新,然后重新调用 render 生成 virtualDom,再通过 diff 方法更新。
  2. 更新前后不是同一个组件,则直接调用 mountElement 渲染新的组件。

通过key进行节点对比
通过key对比节点的实现分为以下几个步骤:

  1. 将真实Dom中具有key属性的子节点存入到一个对象。
  2. 遍历新的节点的子节点时,
    1. 子节点如果存在key属性,则先从对象中查找是否有对应key属性的dom,如果有则调整位置,然后通过diff渲染节点差异。如果没有则将子节点插入当前索引的元素之前。
    2. 没有key属性,也需要将子节点插入当前索引的元素之前。
  3. 遍历对象中的所有属性,将没有在新的子节点对应的key属性的节点删除。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值