记(实现一个mini-react)
准备工作
需要用到的模板文件的仓库地址
1. JSX
先看看jsx语法,做了什么事情 babel.js
文章图片
可以看到,这些jsx语法,经过babel转译后,最终调用了React.createElement
,其需要三个参数type, props, children
。其返回值就是virtual DOM对象。也就是说,我们可以使用babel
将我们的jsx
代码,转换成虚拟DOM, 但是我们需要实现一个自己的createElement
方法
2. 项目配置
查看仓库地址,可以直接获取到模板文件。这里主要介绍一下我们的.babelrc
中如何配置,帮助我们解析jsx代码,并自动的调用我们自己写的createElement
方法
可以看看babel官网 是如何配置react
的。
文章图片
presets
中配置@babel/prset-react
,我们将使用他来转换我们代码中的jsx代码。想想上面的代码,我们写的函数式组件,或者jsx
代码,都被转换成了React.createElement
代码,所以我们借助babel
就可以实现我们自定义的createElement
功能
{
"presets": [
"@babel/preset-env",
[
"@babel/preset-react",
{
"pragma": "MyReact.createElement" //默认的pragma就是 React.createElement,也就是说,我们要实现一个我们的MyReact.createElement, 那么就需要在这里写成MyReact.createElement (only in classic runtime)
}
]
]}
3. Virtual DOM
3.1 什么是Virtual DOM 使用javascript对象来描述真实的dom对象,其结构一般就是这样
vdom = {
type: '',
props: {} // 属性对象
children: [] // 子元素,子组件
}
3.2 创建Virtual DOM 3.2.1 实现一个
createElement
方法
在我们的模板文件中, 已经使用webpack
配置好了代码的入口文件,安装好依赖,然后将项目运行起来,这时候浏览器啥都没有发生。解析jsx
的工作也都有babel
帮我们完成了如果你出现了这种情况,那么请自行更改
webpack
中devserver的端口号文章图片
这是我们的项目目录结构:
文章图片
同时,也可以看看我们项目的目录结构, 这里我已经添加了一个
createElement.js
的文件,我们将在这个文件中,实现将jsx代码,转换为virtual DOM
对象。上面我们提到过,
React.createElement
会接收三个参数type, props, children
,然后会自动的将jsx
代码转换成下面这个类型,因此我们需要做的就是提供这么一个方法,接收这三个参数,然后在将其组装成我们想要的对象。vdom = {
type: '',
props: {} // 属性对象
children: [] // 子元素,子组件
}
- 首先在MyReact文件夹下创建
createDOMElement.js
,他的结构我们上面提到过,接收三个参数,并且返回一个vdom的对象
export default function createElement(type, props, ...children) { return { type, props, children } }
- 创建好了createElement方法,那么我们需要往外暴露,因此在
MyReact/index.js
中,我们将其暴露出来
// MyReact/index.js import createElement from './createElement' export default { createElement, }
- 然后我们在入口文件, 引入我们的
MyReact
,同时写一段jsx
的代码,看看能不能符合我们的预期
// index.jsimport MyReact from "./MyReact" // 按照react的使用方法,这里我们先引入我们自定义的MyReact 此处的MyReact会将jsx语法 通过调用MyReact.createElement(),然后返回我们所需要的VDOM // 这个是在.babelrc配置的 const virtualDOM = ( 你好 Tiny React (我是文本) 嵌套1 嵌套 1.1 (观察: 这个将会被改变)
{2 == 1 && 如果2和1相等渲染当前内容} {2 == 2 && 2} 这是一段内容 这个将会被删除
2, 3 ) console.log(virtualDOM)
看看打印结果,是不是我们的预期
文章图片
bingo,确实是我们想要的。这里大家可以看到
- children中,有些节点是一个
boolean
,还有就是我们可能节点就是个null, 不需要转换 - children中,有些节点直接就是文本,需要转换文文本节点
- props中,需要可以访问children节点
上述两个特殊情况都没有被正确的转换成vDOM,因此我们接下来需要做的就是,对children节点,在进行一次createElement
的操作。
3.2.2 改进createElement
方法上面我们说到,我们需要递归的调用createElement方法去生成vDOM。根据上面三个问题,我们可以作如下改进
export default function createElement(type, props, ...children) { // 1.循环children对象进行对象转换,如果是对象,那么在调用一次createElement方法,将其转换为虚拟dom,否则直接返回,因为他就是普通节点. const childrenElements = [].concat(...children).reduce((result, child) => { // 2.节点中还有表达式, 不能渲染 null节点 boolean类型的节点, 因此我们这里用的reduce,而不是用map if (child !== true && child !== false && child !== null) { // child已经是对象了,那么直接放进result中 if (child instanceof Object) { result.push(child) } else { // 如果他是文本节点,则直接转换为为本节点 result.push(createElement("text", { textContent: child })) } } return result }, []) // 3. props 可以访问children节点, return { type, props: Object.assign({ children: childrenElements }, props), // hebing Props children: childrenElements } }
createElement
就结束了文章图片
3.3 实现render方法 3.3.1 render方法 首先在
MyReact
文件夹下创建render.js
。在render中,我们还要一个
diff
方法,diff
算法就是保证视图只更新变动的部分,需要将新旧dom进行对比(vDOM, oldDOM),然后更新更改部分的dom(container)。我们先写一个diff
方法,实际的算法,我们留在后面来补充。// MyReact/render.jsimport diff from "./diff"
export default function render(vDOM, container, oldDOM) {
// diff算法
diff(vDOM, container, oldDOM)
}
【记(实现一个mini-react)】然后,我们在
MyReact/index.js
将render方法进行导出。import createElement from './createElement'
import render from './render'
export default {
createElement,
render
}
3.3.2 diff方法 刚刚的分析,我们可以知晓,这个
diff
算法是需要三个参数的,newDom, container, oldDom
, 在这里,我们需要做的是就是对比新旧dom,这里,我们需要一个方法,用来创建元素,于是我们现在又需要一个 mountElement
方法,于是创建文件mountElement.js
,用于创建元素。
// MyReact/diff.js
import mountElement from "./mountElement"export default function diff(vDOM, container, oldDOM) {
// 判断oldDOM是否存在
if (!oldDOM) {
// 创建元素
mountElement(vDOM, container)
}
}
3.3.3 mountElement方法 我们的元素,需要区分原生dom元素还是组件。组件分为class组件,以及函数组件。在这我们先把原生dom进行渲染。
- mountElement
- mountNativeElement
- mountComponentElement
- class组件
- 函数组件
// MyReact/mountElement.jsexport default function mountElement(vDOM, container) {
// 此处需要区分原生dom元素还是组件,如何区分? 这个逻辑我们后面再补充
mountNativeElement(vDOM, container)
}
3.3.4 mountNativeElement方法 在这个方法中,我们需要将virtual DOM转成真正的DOM节点,在这里,我们借助一个方法,来创建真实DOM元素,然后再将其append到容器中。
// MyReact/mountNativeElement.js
import createDOMElement from "./createDOMElement"
/**
* 渲染vdom到指定节点
* @param {*} vDOM
* @param {*} container
*/
export default function mountNativeElement(vDOM, container) {
let newElement= createDOMElement(vDOM)
container.appendChild(newElement)
}
下面我们来实现这个
createDOMElement
方法,因为后续我们也会用到,所以把它作为一个公共的函数,方便其他地方使用。这个方法,我们需要做下面的几件事情。
- 将传进来的
vDOM
创建成html
元素 - 创建html元素 又分为两种情况, 纯文本节点,还是元素节点
- 递归创建子节点的html元素
// MyReact/createDOMElement.js import mountElement from "./mountElement" /** * 创建虚拟dom * @param {*} vDOM * @returns */ export default function createDOMElement(vDOM) { let newElement = null// 1. 渲染文本节点, 根据我们之前处理的,纯文本节点,通过text去标记, 值就是props中的textContent if (vDOM.type === 'text') { newElement = document.createTextNode(vDOM.props.textContent) } else { // 2.渲染元素节点 newElement = document.createElement(vDOM.type) // type 就是html元素类型 div input p这些标签等等 // 注意,这里我们只渲染了节点,并没有将props的属性,放在html标签上,这个我们后面在进行 } // 以上步骤仅仅只是创建了根节点,还需要递归创建子节点 vDOM.children.forEach(child => { // 将其放置在父节点上, 由于不确定当前子节点是组件还是普通vDOM,因此我们再次调用mountElement方法,当前的节点容器,就是newElement mountElement(child, newElement) }) return newElement }
代码已经就绪,我们去浏览器看看有没有什么变化, 这个时候,你的浏览器应该长这样了。然后我们再来分析下,我们还缺什么?
文章图片
jsx
的代码,渲染到了页面上。但是现在看看我们的虚拟DOM的结构。跟我们的预期还缺少了下面的东西className
没有被渲染为 classdata-test
type
value
等这些原生属性没有被添加到对应的标签上button
的响应事件
updateNodeElement
方法。还是先创建
MyReact/updateNodeElement.js
这个文件。思考一个问题,我们什么时候调用这个方法来更新node的属性呢?在上面 3.3.4中,我们在进行更新节点的步骤,因此更新node节点的属性,也需要在那里进行
然后可以肯定的是,我们需要两个参数,一个是容器
container
,一个是我们的虚拟DOM
,这样才能确定一个完整的element
.接下来的工作就是要把props属性,依次的赋值给html。回忆一下,如何设置html的属性?我们使用
element.setAttribute('prop', value)
来实现.明确了如何更新html上面的属性,接下来来分析下,我们要处理哪些属性,和事件
首先我们需要遍历当前vDOM的props属性,根据
键值
确定使用何种设置属性的方式- 事件绑定:我们绑定事件都是以on开头,类似这样 onClick;
value
checked
这样的属性值,就不能使用setAttribute
方法了,想想我们使用原生dom的时候,对于输入框这样的value值,我们是直接用的input.value
设置输入框的值;children
属性,这是我们之前手动添加的节点属性,因此,我们要把children
给他剔除;className
属性,这个需要我们将其改为class
,剩下的属性,就可以直接使用键来设置了
代码实现如下:
// MyReact/updateNodeElement.js export default function updateNodeElement (newElement, vDOM) { const { props } = vDOM // 遍历vdom上的key,获取每个prop的值, Object.keys(props).forEach(key => { const currentProp = props[key] // 如果是以on开头的,那么就认为是事件属性,因此我们需要给他注册一个事件 onClick -> click if (key.startsWith('on')) { // 由于事件都是驼峰命名的,因此,我们需要将其转换为小写,然后取最后事件名称 const eventName = key.toLowerCase().slice(2) // 为当前元素添加事件处理函数 newElement.addEventListener(eventName, currentProp) } else if (key === 'value' || key === 'checked') { // input 中的属性值 newElement[key] = currentProp } else if (key !== 'children') { // 抛开children属性, 因为这个是他的子节点, 这里需要区分className和其他属性 newElement.setAttribute(key === 'className' ? 'class' : key, currentProp) } }) }
接下来,我们找到createDOMElement.js
文件,我们需要在渲染元素节点后,更新他的属性值
// MyReact/createDOMElement.js import mountElement from "./mountElement" /** * 创建虚拟dom * @param {*} vDOM * @returns */ export default function createDOMElement(vDOM) { let newElement = null// 1. 渲染文本节点, 根据我们之前处理的,纯文本节点,通过text去标记, 值就是props中的textContent if (vDOM.type === 'text') { newElement = document.createTextNode(vDOM.props.textContent) } else { // 2.渲染元素节点 newElement = document.createElement(vDOM.type) // type 就是html元素类型 div input p这些标签等等 // 更新dom元素的属性,事件等等 updateNodeElement(newElement, vDOM) } // 以上步骤仅仅只是创建了根节点,还需要递归创建子节点 vDOM.children.forEach(child => { // 将其放置在父节点上, 由于不确定当前子节点是组件还是普通vDOM,因此我们再次调用mountElement方法,当前的节点容器,就是newElement mountElement(child, newElement) }) return newElement }
至此,我们已经完成了属性设置,现在回到浏览器看看,我们的结果,class
属性被正确的加载了元素上面,其他属性,以及事件响应,也都做好了。
文章图片
文章图片
阶段一小结
jsx
语法在babel
加持下,可以转换成vDOM
,我们使用babel-preset-react
,并配置.babelrc
,可以让我们实现自定义的createElement
方法。然后将jsx
转换成虚拟DOM
.- 我们通过
createElement
方法生成的虚拟DOM对象,通过diff
算法(本文未实现),然后进行dom
的更新 - 虚拟dom对象需要我们对所有的节点进行真实dom的转换。
- 创建节点我们需要使用
element.createElement(type)
创建文本节点element.createElement(type)
, 设置属性,我们需要用到element.setAttribute(key, value)
这个方法。 - 节点更新需要区分html节点,组件节点。组件节点又需要区分 class组件以及函数组件
可以看到,我们仅仅实现了
jsx
代码能够被正确的渲染到页面中,我们还有很多工作未做,比如下面的这些,后续的代码更新都放在这里了。源码- 组件渲染函数组件、类组件
- 组件渲染中 props的处理
- dom元素更新时的vDOM对比,删除节点
- setState方法
- 实现ref属性获取dom对象数组
- key属性的节点标记与对比
推荐阅读
- 一个人的旅行,三亚
- 一个小故事,我的思考。
- EffectiveObjective-C2.0|EffectiveObjective-C2.0 笔记 - 第二部分
- 一个人的碎碎念
- 野营记-第五章|野营记-第五章 讨伐梦魇兽
- 20170612时间和注意力开销记录
- 2018年11月19日|2018年11月19日 星期一 亲子日记第144篇
- 七年之痒之后
- 叙述作文
- 我从来不做坏事