實現createElement函數
上一章中有講到,js文件可以識別jsx語法,是利用插件調用 react內部的createElement方法,傳遞相應的參數,生成虛擬的dom對象。在自己搭建react-source項目中,我們就先建一個文件夾react,然後創建一個index.js文件
index.js文件暴露一個React對象,這個對象上面一定有一個方法,createElement方法。我們就先這麼寫:
const React = {
createElement
}
// 生成虛擬的dom對象
function createElement(tag, attrs, ...children) {
return {
tag,
attrs,
children
}
}
export default React;
再建一個react-dom文件夾。
src下面的index.js引入新建好的文件:
import React from './react'
import ReactDOM from './react-dom'
const ele = (
<div className='active'>
hello <h1>react</h1>
</div>
)
console.log(ele);
ReactDOM.render(ele, document.getElementById('app'));
然後npm run dev,打開頁面,看到控制檯打印出來這樣的一個對象,這就我們所說的虛擬dom對象,createElement方法是在es6轉es5的過程中,插件去調用的,並且解析相應的參數傳遞給這個方法。
可以看到,createElement這個函數很簡單,只需要把傳遞進來的參數返回就好,這樣jsx轉換成了虛擬的DOM對象。
但是,看下頁面,並沒有任何展示,這是因爲react-dom中的render函數沒有寫任何的功能,也就是沒有把虛擬的DOM轉化爲html。
實現render函數
調用render的時候傳遞的第一個參數是ele,也就是一個虛擬的dom對象,這個值有幾種情況:
1,參數沒有傳遞
創建一個空的文本節點document.createTextNode('');
2,傳遞的是字符串,是一個文本節點
document.createTextNode(v),可以直接創建文本節點
3,傳遞的是虛擬的dom對象
獲取虛擬對象中的屬性
const { tag = '', attrs = '', children = [] } = v;
分析一下tag情況
a:tag是dom標籤
直接創建一個dom元素,document.createElement(tag),標籤上可能有屬性,標籤內部還會有子節點,實現一個setArribute函數處理dom上的屬性。setArribute調用的時候,傳遞創建好的dom標籤,屬性和對應的屬性值。
屬性分4種情況:
- class
- style
- 事件
- 自定義屬性。
class
// class
if(key === 'className') {
key = 'class';
}
事件
這種情況需要把onClick大寫的事件轉化爲原生的小寫,添加到dom上。
// 事件
if(/on\w+/.test(key)) {
key = key.toLowerCase()
dom[key] = value || ''
}
樣式 style
- style的值是字符串
- style的值是對象
字符串;
<body>
<div id='app' style="color: red"></div>
</body>
<script>
var app = document.getElementById('app');
console.log(app.style.cssText);
</script>
所以可以直接這麼處理:
if(!value || typeof value === 'string') {
dom.style.cssText = value || ''
}
如果是對象,需要枚舉屬性,判斷屬性值,如果是number類型的,需要拼接'px'。
自定義屬性
原來dom上存在相同的自定義屬性,可以直接替換
原來dom上沒有,並且傳遞了value值,則需要設置屬性dom.setAttribute(key,value)
咩有傳遞value,是需要移除這個自定義屬性, dom.removeAttribute(key)
完成的代碼如下:
function setArribute(dom, key, value) {
// class
if(key === 'className') {
key = 'class';
}
// 事件
if(/on\w+/.test(key)) {
key = key.toLowerCase()
dom[key] = value || ''
} else if(key === 'style') { // 樣式
if(!value || typeof value === 'string') {
dom.style.cssText = value || ''
} else if(value && typeof value === 'object') {
for(let k in value) {
if(typeof value[k] === 'number') {
dom.style[k] = value[k] + 'px'
} else {
dom.style[k] = value[k]
}
}
}
} else {
if(key in dom) {
dom[key] = value;
}
if(value) {
dom.setAttribute(key,value)
} else {
dom.removeAttribute(key)
}
}
}
如果說傳遞的是一個函數組件,看下tag會是什麼?
import React from './react'
import ReactDOM from './react-dom'
function Home() {
return (
<div className='home'>
hello <h1>react</h1>
</div>
)
}
console.log(<Home title='home'/>)
ReactDOM.render(<Home title='home'/>, document.getElementById('app'));
可以看到tag是一個函數。
如果說是一個class 類呢?
import React from './react'
import ReactDOM from './react-dom'
class Home extends React.Component {
render() {
return (
<div className='active'>
'hello'
<h1 >react</h1>
</div>
)
}
}
console.log(<Home title='home' />)
ReactDOM.render(<Home title='home' />, document.getElementById('app'));
注意:也可以在react腳手架創建的項目種看下打印結果。
tag也是一個函數,所以還有一種情況是tag是函數。
b, tag是函數
函數組件也轉化成了虛擬的DOM,只不過這個虛擬對象的tag是一個函數。
- 創建一個組件
- 設置組件的屬性
把tag和attrs傳遞給createComponent函數。這個函數接收到參數以後,需要先判斷一下這個tag是class類生成的,還是函數生成的。
區分函數組件和class組件:函數組件沒有render函數,而class類組件原型上有render函數。
類組件:直接new comp(props)創建一個實例,屬性傳遞過去。
函數組件:定義一個空的class類,創建一個實例,修改實例的constructor,給實例增加一個render方法,內部調用函數組件,傳遞props。
function createComponent(comp, props) {
let inst;
// 如果是類定義的組件
if(comp.prototype && comp.prototype.render) {
return inst = new comp(props);
}
// 如果是函數,這裏我們需要轉化爲類,方便後面統一管理
inst = new Component(props);
inst.constructor = comp;
inst.render = function() {
return this.constructor(props);
}
return inst;
}
src/react/component.js
class Component {
constructor(props = {}){
this.props = props;
this.state={}
}
}
export default Component;
組件創建好以後,還需要渲染組件,把組件變成真是的dom。先給組件設置一個props屬性,再來渲染組件。
怎麼把組件html節點變成虛擬的dom的對象呢?組件內部都有render函數,返回一串jsx代碼,可以調用這個函數拿到jsx代碼快,也就生成虛擬的dom對象了。
給組件實例增加一個屬性props。
function setComeponentProps(comp, props) {
// 設置組件的props
comp.props = props;
renderComponent(comp)
}
然後渲染組件
function renderComponent(comp) {
// v虛擬的dom對象
const v = comp.render();
// 生成真實的dom
comp.base = _render(v);
}
4,children有值
如果children是有值的,說明還有字節點需要渲染,這時只需要循環遞歸調用_ender就可以了。
初步代碼如下:
function _render(v) {
if(v === undefined || v === null || typeof v === 'boolean') return document.createTextNode('');
// 如果是數值
if(typeof v === 'number') v = String(v)
// 如果是字符串
if(typeof v === 'string') {
return document.createTextNode(v);
}
// 如果tag是函數,則渲染組件
const { tag = '', attrs = '', children = [] } = v;
if(typeof tag === 'function') {
// 創建組件
const comp = createComponent(tag, attrs)
// 設置組件屬性
setComeponentProps(comp, attrs)
// 組件渲染的節點返回
return comp.base;
}
// 是一個虛擬dom對象
// const { tag = '', attrs = '', children = [] } = v;
const dom = document.createElement(tag);
if(attrs) {
Object.keys(attrs).forEach((key) => {
const value = attrs[key];
setArribute(dom, key, value);
})
}
isArray(children) && children.forEach((child)=>render(child, dom))
return dom
}
function isType(type) {
return function(obj) {
return {}.toString.call(obj) == "[object " + type + "]"
}
}
var isObject = isType("Object")
var isString = isType("String")
var isArray = Array.isArray || isType("Array")
var isFunction = isType("Function")
var isUndefined = isType("Undefined")
看,我們的頁面又能正常展示了。此時的src/index.js