这是我参与更文挑战的第9天,活动详情查看: 更文挑战
前面我们已经将virtualDOM转化为真实DOM了,但是这些真实DOM是没有任何属性的,接下来我们就为真实DOM添加属性
为真实DOM添加属性
分析: 这些属性全部存储在虚拟DOM的props里面,我们只需要在创建元素的时候找到virtualDOM的属性props,然后遍历props属性,在遍历的过程当中就可以将这些属性添加给真实DOM元素了,但是在添加属性的时候我们还需要考虑一些不同的情况,因为不同情况我们要去做不同的处理,如果是事件属性,那么我们就要为该元素去添加事件;还要判断这个属性是否是value属性或者是checked属性,因为这两个属性是无法使用setAttribute方法去设置它的,我们还需要看看这个属性是否是children属性,children不是属性是子元素但是也存在props中;还需要看这个属性是否是className属性,如果是className的话我们就给元素添加class属性;如果是普通属性就可以用setAttribute方法给真实DOM去设置就行了。
代码实现:
export default function updateNodeElement(newElement, virtualDOM) {
// 获取节点对应的属性对象
const newProps = virtualDOM.props
Object.keys(newProps).forEach(propName => {
// 获取属性值
const newPropsValue = newProps[propName]
// 判断属性是否是事件属性 onClick => click
if(propName.slice(0, 2) === "on") {
const eventName = propName.toLowerCase().slice(2)
// 为元素添加事件
newElement.addEventListener(eventName, newPropsValue)
} else if(propName === "value" || propName === "checked") {
newElement[propName] = newPropsValue
} else if (propName !== "children") {
if(propName === "className") {
newElement.setAttribute('class', newPropsValue)
} else {
newElement.setAttribute(propName, newPropsValue)
}
}
})
}
在生成真实DOM文件夹引用这个方法:
import updateNodeElement from "./updateNodeElement"
export default function mountNativeElement (virtualDOM, container) {
let newElement = null
···
else {
// 元素节点
newElement = document.createElement(virtualDOM.type);
updateNodeElement(newElement, virtualDOM);
小结:
当我们创建真实元素节点的时候,调用updateNodeElement给这个元素添加属性,属性存在virtualDOM的props中,所以这个updateNodeElement传两个参数,一个是当前的真实DOM,newElement(给谁设置属性),一个是virtualDOM(这些属性藏在哪),在把props里面的属性全部拿出来,属性为事件使用addEventListener添加监听事件,value和checked使用newElement[propName] = newPropsValue,接下来除children这个节点,其他节点为className转化为class,其他的直接setAttribute。
组件渲染之区分函数组件和类组件
组件在转化为virtualDOM之前,会是什么样子,因为无论我们在渲染组件还是渲染DOM元素我们都要调用mountElement这个方法。在这个方法中我们要把组件和普通virtualDOM元素区分开,因为在普通virtualDOM元素他的处理方式和组件是不一样的
// 原始组件
const Heart = () => <span>♥</span>
调用组件方式
<Heart />
下面是组件的VirtualDOM的
type: f function() {},
props: {}
children: []
分析:
我们发现组件type是一个函数,而普通VirtualDOM是一个字符串。所以在调用mountElement分开处理,组件执行方法是mountComponent(virtualDOM, container),普通virtualDOM执行的方法是mountNativeElement(virtualDOM, container),无论类组件还是函数组件,其实本质都是函数,如果virtualDOM的type属性值为函数,就说明当前这个VirtualDOM为组件。
代码:
import isFunction from './isFunction';
import mountComponent from './mountComponent';
import mountNativeElement from './mountNativeElement';
export default function mountElement(virtualDOM, container) {
// Component VS NativeElement
if(isFunction(virtualDOM)) {
// 处理 组件的方法 mountComponent
mountComponent()
} else {
// 处理原生virtualDOM方法mountNativeElement
mountNativeElement(virtualDOM, container);
}
}
// isFunction.js
// 区分virtualDOM的type是否是函数
export default function isFunction(virtualDOM) {
return virtualDOM && typeof virtualDOM.type === "function"
}
上面我们区分了组件virtualDOM和普通virtualDOM,我们前面已经写完普通VirtualDOM处理方法,现在编写组件VirtualDOM处理方法,组件分两种类组件和函数组件,虽然本质都是函数组件,但是还是要分开处理更为清晰一些。于是在创建一个文件 isFuntionComponent.js创建同名函数用来判断这个组件Virtual是函数还是类,区分开之后函数组件调用函数组件处理方法,类组件调用类组件处理方法
import isFunctionComponent from "./isFunctionComponent";
export default function mountComponent(virtualDOM, container) {
// 判断组件是类组件还是函数组件
if(isFunctionComponent(virtualDOM)) {
console.log('函数组件。。。')
} else {
console.log('类组件')
}
}
// isFunctionComponent.js
import isFunction from "./isFunction"
export default function isFunctionComponent(virtualDOM) {
const type = virtualDOM.type
return (type && isFunction(virtualDOM) && !(type.prototype && type.prototype.render))
}
函数组件走函数组件处理方法buildFunctionComponent, 类组件走类组件处理方法buildClassComponent,在buildFunctionComponent 方法中直接调用virtualDOM.type()就可以拿到对应的虚拟DOM,因为函数组件 type就是一个方法,
import isFunctionComponent from "./isFunctionComponent";
import mountNativeElement from "./mountNativeElement";
export default function mountComponent(virtualDOM, container) {
let nextVirtualDOM = null
// 判断组件是类组件还是函数组件
if(isFunctionComponent(virtualDOM)) {
nextVirtualDOM = buildFunctionComponent(virtualDOM);
console.log("nextVirtualDOM", nextVirtualDOM);
}
mountNativeElement(nextVirtualDOM, container)
}
function buildFunctionComponent(virtualDOM) {
return virtualDOM.type()
}
// src/index.js
function Heart() {
return <div>♥</div>
}
TinyReact.render(<Heart />, root)
得到结果是:
函数组件已经初步成功渲染成功,但是我们还是有一个问题没有解决,如果这个函数组件里面调用的不是原始标签是组件的话,我们还不能直接调用
mountNativeElement进行处理,如果是组件的话还是要递归调用mountComponent方法,知道不是isFunction,type不是函数为止即为普通VirtualDOM
// mountComponent.js
export default function mountComponent(virtualDOM, container) {
···
if(isFunction(nextVirtualDOM)) {
mountComponent(nextVirtualDOM, container);
} else {
mountNativeElement(nextVirtualDOM, container);
}
}
function Dome(){
return <div>Hello</div>
}
function Heart() {
return <Demo />
}
TinyReact.render(<Heart />, root)
效果:
好我们上面已经处理完了函数组件,接下来处理下函数组件的props属性. 只需要在buildFunctionComponent方法里面的virtualDOM.type这个函数里面将virtualDOM.props传递进去即可,当这个函数接收的时候在JSX中使用就会转化成相应的节点,渲染出来
function buildFunctionComponent(virtualDOM) {
return virtualDOM.type(virtualDOM.props || {});
}
function Demo(){
return <div>Hello</div>
}
function Heart(props) {
return <div>{props.title}♥<Demo /></div>
}
TinyReact.render(<Heart title="Hello React" />, root)
上面讲完了函数组件的渲染,接下来我们来处理类组件的渲染
类组件渲染
分析:
我们上面说了,组件处理两种处理如果是函数组件我们调用buildFunctionComponent, 如果是类组件我们用类组件处理函数buildClassComponent,我们会创建一个基类Component类属于TinyReact, 使所有声明的类组件继承该Component类。buildClassComponent和函数组件一样,babel处理的时候类组件type也是一个函数,但是这个函数是构造函数,new来及时组件virtualDOM,然后调用这个实例render即可
import isFunction from "./isFunction";
import isFunctionComponent from "./isFunctionComponent";
import mountNativeElement from "./mountNativeElement";
export default function mountComponent(virtualDOM, container) {
let nextVirtualDOM = null
···
else {
// 类组件
nextVirtualDOM = buildClassComponent(virtualDOM)
}
if(isFunction(nextVirtualDOM)) {
mountComponent(nextVirtualDOM, container);
} else {
mountNativeElement(nextVirtualDOM, container);
}
}
function buildClassComponent(virtualDOM) {
const component = new virtualDOM.type()
const nextVirtualDOM = component.render()
return nextVirtualDOM
}
// Component.js
export default class Component {
}
// TinyReact/index.js
mport createElement from "./createElement"
import render from "./render"
import Component from "./Component"
export default {
createElement,
Component,
render
}
// src/index.js 调用Demo
class Alert extends TinyReact.Component {
render () {
return <div> Hello TinyReact</div>
}
}
TinyReact.render(<Alert />, root)
处理类组件的Props属性
我们使用react类组件应该是这样的
class Alert extends TinyReact.Component {
render () {
return <div>
{this.props.name}
{this.props.age}
</div>
}
}
TinyReact.render(<Alert name="张三" age={20}/>, root)
这个props明显是类的属性,但是我们在使用react类组件的时候会给类组件添加props组件么,很明显是没有的对吧,那实现思路是啥呢,props这个属性可以放到Component中去完成初始化,在子组件构造器里面去调用即可
// Component.js
export default class Component {
constructor (props) {
this.props = props
}
}
class Alert extends TinyReact.Component {
constructor (props) {
super(props)
}
render () {
return <div>
{this.props.name}
{this.props.age}
</div>
}
}
TinyReact.render(<Alert name="张三" age={20}/>, root)
所有每个类组件的构造器都接受一个props参数我们应该在buildClassComponent 将其传递过来即可
function buildClassComponent(virtualDOM) {
const component = new virtualDOM.type(virtualDOM.props || {});
const nextVirtualDOM = component.render()
return nextVirtualDOM
}
好!今天学习到这,我们今天主要学习了虚拟DOM到真实DOM的渲染, 也可以分为两大类普通VirtualDOM和组件virtualDOM,我们真正处理呢还是要分为三大类,普通VirtualDOM, 函数组件和类组件VirtualDOM,三种虚拟DOM需要处理,这三种处理都有彼此的不同之处,但是都要明确的步骤先处理基本结构完成渲染,然后再处理他们各自情况下的props,这里比较麻烦和基础的是普通virtualDOM的props的处理,因为类组件和函数组件经过转化之后都用到它,普通VirtualDOM 的props处理三种情况,事件,处理掉true,false, null不显示的文本节点,表单控件的value和checked的处理。完成VirtualDOM的基本机构和props的处理,就完成了我们今天主要的目标,VirtualDOM到RealDOM的转化和渲染
接下来我们会学习更新DOM元素,敬请期待!!!欢迎点赞与关注,加油