筹备工作
须要用到的模板文件的仓库地址
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.jsimport createElement from './createElement'export default { createElement,}
而后咱们在入口文件, 引入咱们的
MyReact
,同时写一段jsx
的代码,看看能不能合乎咱们的预期// index.jsimport MyReact from "./MyReact"// 依照react的应用办法,这里咱们先引入咱们自定义的MyReact 此处的MyReact会将jsx语法 通过调用MyReact.createElement(),而后返回咱们所须要的VDOM// 这个是在.babelrc配置的const virtualDOM = ( <div className="container"> <h1>你好 Tiny React</h1> <h2 data-test="test">(我是文本)</h2> <div> 嵌套1 <div>嵌套 1.1</div> </div> <h3>(察看: 这个将会被扭转)</h3> {2 == 1 && <div>如果2和1相等渲染以后内容</div>} {2 == 2 && <div>2</div>} <span>这是一段内容</span> <button onClick={() => alert("你好")}>点击我</button> <h3>这个将会被删除</h3> 2, 3 <input type="text" value="13" /> </div>)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}}
当初再看看咱们的输入, 能够看到,之前咱们children中有false的,以及纯文本的节点,都被正确的解决了,到这里,咱们的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)}
而后,咱们在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.jsimport 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.js export default function mountElement(vDOM, container) { // 此处须要辨别原生dom元素还是组件,如何辨别? 这个逻辑咱们前面再补充 mountNativeElement(vDOM, container)}
3.3.4 mountNativeElement办法
在这个办法中,咱们须要将virtual DOM转成真正的DOM节点,在这里,咱们借助一个办法,来创立实在DOM元素,而后再将其append到容器中。
// MyReact/mountNativeElement.jsimport 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.jsimport 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}
代码曾经就绪,咱们去浏览器看看有没有什么变动, 这个时候,你的浏览器应该长这样了。而后咱们再来剖析下,咱们还缺什么?
3.3.5 更新节点属性的办法(updateNodeElement)
咱们当初曾经实现了将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.jsexport 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.jsimport 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属性的节点标记与比照