在工做中咱們一直使用react及react相關框架(antd/antd-mobile)css
可是對於react的深層的瞭解不夠:JSX語法與虛擬DOM的關係?高階組件是如何實現的?dom diff的原理?html
經過寫一篇react小冊來查缺補漏。node
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(
<label className="test" htmlFor='hello'>
hello<span>world</span>
</label>,
document.getElementById('root')
);
複製代碼
使用ReactDOM.render
,第一個參數傳入JSX語法糖,第二個參數傳入container,能簡單實如今document上建立h1 dom節點。react
其實,內部的執行方式以下:git
import React from 'react';
import {render} from 'react-dom';
render(
React.createElement(
'label',
{htmlFor:'hello',className:'test'},
'hello',
React.createElement(
'span',
null,
'world'
)
),
document.getElementById('root')
);
複製代碼
因此ReactDOM.render的時候,看似引入的React沒有用,但必須引入由於用到了React,createElement方法。github
render出的HTML:數組
<label for="hello" class="test">hello<span>world</span></label>
複製代碼
爲了瞭解react createElement的流程,咱們看一下源碼:bash
var React = {
...
createElement: createElementWithValidation,//React上定義了createElement方法
...
}
function createElementWithValidation(type, props, children) {
var element = createElement.apply(this, arguments);
...//校驗迭代器數組是否存在惟一key
...//校驗fragment片斷props
...//props type校驗
return element
}
function createElement(type, config, children) {
var propName = void 0;
// Reserved names are extracted
var props = {};
var key = null;
var ref = null;
var self = null;
var source = null;
...
return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);
}
複製代碼
從React.createElement
開始執行,完成了props、types等參數校驗,迭代
展開childrens的參數:type, props, key, ref, self, source。返回了一個相似於babel語法樹結構的嵌套對象(只是我的認爲...),如圖下:babel
咱們保留返回對象中最關鍵的屬性(type,props),而後簡化createElement方法便於理解:antd
function ReactElement(type,props) {
this.type = type;
this.props = props;
}
let React = {
createElement(type,props={},...childrens){
childrens.length===1?childrens = childrens[0]:void 0
return new ReactElement(type,{...props,children:childrens})
}
};
複製代碼
React.createElement返回的是一個含有type(標籤),和它標籤屬性和內部對象(children)的Object,做爲參數傳給ReactDom.render()方法
{
props:{
childrens:['text',{type:'xx',props:{}}]
name:'xx'
className:'xx'
}
type:'xx'
}
複製代碼
因而咱們能夠根據ReactDom.render()的入參,簡寫出它的實現方法。
let render = (vNode,container)=>{
let {type,props} = vNode;
let elementNode = document.createElement(type); // 建立第一個元素
for(let attr in props){ // 循環全部屬性
if(attr === 'children'){ // 若是是children表示有嵌套關係
if(typeof props[attr] == 'object'){ // 看是不是隻有一個文本節點
props[attr].forEach(item=>{ // 多個的話循環判斷 若是是對象再次調用render方法
if(typeof item === 'object'){
render(item,elementNode)
}else{ //是文本節點 直接建立便可
elementNode.appendChild(document.createTextNode(item));
}
})
}else{ // 只有一個文本節點直接建立便可
elementNode.appendChild(document.createTextNode(props[attr]));
}
}else{
elementNode = setAttribute(elementNode,attr,props[attr])
}
}
container.appendChild(elementNode)
};
function setAttribute(dom,name,value) {
if(name === 'className') name = 'class'
if(/on\w+/.test(name)){
name = name.toLowerCase();
dom[ name ] = value || '';
}else if ( name === 'style' ) {
if ( !value || typeof value === 'string' ) {
dom.style.cssText = value || '';
} else if ( value && typeof value === 'object' ) {
for ( let name in value ) {
dom.style[ name ] = typeof value[ name ] === 'number' ? value[ name ] + 'px' : value[ name ];
}
}
}else{
if ( name in dom ) {
dom[ name ] = value || '';
}
if ( value ) {
dom.setAttribute( name, value );
} else {
dom.removeAttribute( name );
}
}
return dom
}
複製代碼
Ract做爲數據渲染DOM的框架,若是用傳統的刪除整個節點並新建節點的方式會很消耗性能。
React渲染頁面的方法時比較對比虛擬DOM先後的變化,再生產新的DOM。
檢查一個節點是否變化,要比較節點自身及它的父子節點,因此查找任意兩棵樹之間最少修改數的時間複雜度是O(n^3)。
React比較只比較當前層(同一顏色層),將比較步驟優化到了接近O(n)。
1、建立dom
優化JSX和虛擬DOM中,createElement
方法:
element.js
let utils = require('./utils')
class Element {
constructor(tagName, attrs, key, children) {
this.tagName = tagName;
this.attrs = attrs;
this.key = key;
this.children = children || [];
}
render() {
let element = document.createElement(this.tagName);
for (let attr in this.attrs) {
utils.setAttribute(element, attr, this.attrs[attr]);
element.setAttribute('key', this.key)
}
let children = this.children || [];
//先序深度遍歷
children.forEach(child => {
let childElement = (child instanceof Element) ? child.render() : document.createTextNode(child);
element.appendChild(childElement);
});
return element;
}
}
複製代碼
class Tree {
constructor(v, children) {
this.v = v
this.children = children || null
}
}
const tree = new Tree(10, [
new Tree(5),
new Tree(3, [new Tree(7), new Tree(11)]),
new Tree(2)
])
module.exports = tree
複製代碼
const tree = require('./1.Tree')
function tree_transverse(tree) {
console.log(tree.v)//10 5 3 7 11 2
tree.children && tree.children.forEach(tree_transverse)
}
tree_transverse(tree)
複製代碼
建立原始dom dom1,插入到頁面。
let ul1 = createElement('ul', {class: 'list'}, 'A', [
createElement('li', {class: 'list1'}, 'B', ['1']),
createElement('li', {class: 'list2'}, 'C', ['2']),
createElement('li', {class: 'list3'}, 'D', ['3'])
]);
let root = dom1.render();
document.body.appendChild(root);
複製代碼
建立節點變化的DOM樹 dom2,修改了dom2的父節點ul的屬性class,新增並修改了子節點的位置
let ul2 = createElement('ul', {class: 'list2'}, 'A', [
createElement('li', {class: 'list4'}, 'E', ['6']),
createElement('li', {class: 'list1'}, 'B', ['1']),
createElement('li', {class: 'list3'}, 'D', ['3']),
createElement('li', {class: 'list5'}, 'F', ['5']),
]);
複製代碼
咱們不能生硬得去直接銷燬dom1,新建dom2。而是應該比較新舊兩個dom,在原始dom上增刪改。
let patches = diff(dom1, dom2,root)
複製代碼
文本節點
比較function diff(oldTree, newTree, root) {
let patches = {};
let index = 0;
walk(oldTree, newTree, index, patches, root);
return patches;
}
function walk(oldNode, newNode, index, patches, root) {
let currentPatch = [];
if (utils.isString(oldNode) && utils.isString(newNode)) {
if (oldNode != newNode) {
currentPatch.push({type: utils.TEXT, content: newNode});
}
}
}
複製代碼
若是文本不一樣,咱們打補丁
,記錄修改的類型和文本內容
··
else if (oldNode.tagName == newNode.tagName) {
let attrsPatch = diffAttrs(oldNode, newNode);
if (Object.keys(attrsPatch).length > 0) {
currentPatch.push({type: utils.ATTRS, node: attrsPatch});
}
} else {
currentPatch.push({type: utils.REPLACE, node: newNode});
}
···
複製代碼
let keyIndex = 0;
let utils = require('./utils');
let allPatches;//這裏就是完整的補丁包
let {Element} = require('./element')
function patch(root, patches) {
allPatches = patches;
walk(root);
}
function walk(node) {
let currentPatches = allPatches[keyIndex++];
(node.childNodes || []).forEach(child => walk(child));
if (currentPatches) {
doPatch(node, currentPatches);
}
}
function doPatch(node, currentPatches) {
currentPatches.forEach(patch=> {
switch (patch.type) {
case utils.ATTRS:
for (let attr in patch.node) {
let value = patch.node[attr];
if (value) {
utils.setAttribute(node, attr, value);
} else {
node.removeAttribute(attr);
}
}
break;
case utils.TEXT:
node.textContent = patch.content;
break;
case utils.REPLACE:
let newNode = (patch.node instanceof Element) ? patch.node.render() : document.createTextNode(patch.node);
node.parentNode.replaceChild(newNode, node);
break;
case utils.REMOVE:
node.parentNode.removeChild(node);
break;
}
})
}
module.exports = patch
複製代碼
進行到這裏,咱們已經完成了父節點的修補。
對於ul的子節點,咱們可使用一樣的方法進行迭代一次。可是咱們推薦用子節點的key來更快速得去判斷是否刪除、新增、順序變換。
在oldTree中,有三個子元素 B、C、D 在newTree中,有四個子元素 E、B、C、D
function childDiff(oldChildren, newChildren) {
let patches = []
let newKeys = newChildren.map(item=>item.key)
let oldIndex = 0;
while (oldIndex < oldChildren.length) {
let oldKey = oldChildren[oldIndex].key;//A
if (!newKeys.includes(oldKey)) {
remove(oldIndex);
oldChildren.splice(oldIndex, 1);
} else {
oldIndex++;
}
}
}
//標記去除的index
function remove(index) {
patches.push({type: utils.REMOVE, index})
}
複製代碼
function childDiff(oldChildren, newChildren) {
...
oldIndex = 0;
newIndex = 0;
while (newIndex < newChildren.length) {
let newKey = (newChildren[newIndex] || {}).key;
let oldKey = (oldChildren[oldIndex] || {}).key;
if (!oldKey) {
insert(newIndex,newChildren[newIndex]);
newIndex++;
} else if (oldKey != newKey) {
let nextOldKey = (oldChildren[oldIndex + 1] || {}).key;
if (nextOldKey == newKey) {
remove(newIndex);
oldChildren.splice(oldIndex, 1);
} else {
insert(newIndex, newChildren[newIndex]);
newIndex++;
}
} else {
oldIndex++;
newIndex++;
}
}
function remove(index) {
patches.push({type: utils.REMOVE, index})
}
...
複製代碼
while (oldIndex++ < oldChildren.length) {
remove(newIndex)
}
複製代碼
function childPatch(root, patches = []) {
let nodeMap = {};
(Array.from(root.childNodes)).forEach(node => {
nodeMap[node.getAttribute('key')] = node
});
patches.forEach(path=> {
let oldNode
switch (path.type) {
case utils.INSERT:
let newNode = nodeMap[path.node.key] || path.node.render()
oldNode = root.childNodes[path.index]
if (oldNode) {
root.insertBefore(newNode, oldNode)
} else {
root.appendChild(newNode)
}
break;
case utils.REMOVE:
oldNode = root.childNodes[path.index]
if (oldNode) {
root.removeChild(oldNode)
}
break;
default:
throw new Error('沒有這種補丁類型')
}
})
}
複製代碼
記錄補丁修改節點結果:
(未完待續~~)