toyReact框架搭建
我们之前在博客上有提到 react的教程中tic-tac-toe游戏的案例,那么在篇博客中,我们主要要实现的是搭建自己的toyReact框架,将tic-tac-toe在toyReact框架中跑起来。
这里是代码的github仓库: https://github.com/feddiyao/toyReact
环境配置
包安装:
npm install --save-dev webpack webpack-cli
npm install --save-dev babel-loader @babel/core @babel/preset-env
npm install @babel/plugin-transform-react-jsx --save-dev
webpack的config文件:
module.exports = {
entry: {
main: './main.js'
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
plugins: ['@babel/plugin-transform-react-jsx']
}
}
}
]
},
mode: "development",
optimization: {
minimize: false
}
}
其中mode
和optimization
的设置增加了页面的可读性,此时执行npx webpack
输出的main.js文件中只有相关代码:
eval("\n\n//# sourceURL=webpack:///./main.js?");
即当我们在浏览器中打开一个main.js文件时,会映射到一个单独的文件,webpack将给我们提供一个易于阅读和调试的版本。
babel
是把 JavaScript 中 es2015/2016/2017/2046 的新语法转化为 es5,让低端运行环境(如浏览器和 node )能够认识并执行。
presets
设定实现的效果:
当我们在main.js里面放入测试代码:
for (let i of [1, 2, 3]) {
console.log(i);
}
webpack的输出man.js文件中相关代码为:
eval("for (var _i = 0, _arr = [1, 2, 3]; _i < _arr.length; _i++) {\n var i = _arr[_i];\n console.log(i);\n}\n\n//# sourceURL=webpack:///./main.js?");
JSX 原理和关键实现
上面的环境配置中 @babel/plugin-transform-react-jsx
是babel专门用来处理jsx的,将jsx转换为react函数
看一下plugins设定实现的效果:
当我们在man.js中放入测试代码:
let a = <div/>
webpack的输出man.js文件中相关代码为:
var a = /*#__PURE__*/React.createElement("div", null);
当我们把plugins进行如下的配置:
plugins: [['@babel/plugin-transform-react-jsx', {pragma: 'createElement'}]]
webpack的输出man.js文件中相关代码为:
var a = createElement("div", null);
可以做一个比较,即在输出时把React.createElement
替换为了createElement
多个子节点进行测试:
let a = <div id="a" class="c">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
webpack的输出man.js文件中相关代码:
var a = createElement("div", {
id: "a",
"class": "c"
}, createElement("div", null), createElement("div", null), createElement("div", null), createElement("div", null));
第一个参数为div
,第二个参数为属性列表,从第三个参数开始就是子节点。
所以我们来初步实现一下createElement:
function createElement(tagName, attributes, ...children) {
let e = document.createElement(tagName);
for (let p in attributes) {
e.setAttribute(p, attributes[p]);
}
for (let child of children) {
e.appendChild(child);
}
return e;
}
此时若我们在工作台打印一下a,则输出为:
在子节点中加入文本的处理:
for (let child of children) {
if(typeof child === "string") {
child = document.createTextNode(child)
}
e.appendChild(child);
}
可以在浏览器中调用以下代码来进行节点的挂载:
document.body.appendChild(a);
接下来考虑MyComponent类型的节点:
首先在createElement
方法中加入类型的判断:
export function createElement(type, attributes, ...children) {
let e;
if (typeof type === "string") {
e = document.createElement(type);
} else {
e = new type;
}
for (let p in attributes) {
e.setAttribute(p, attributes[p]);
}
for (let child of children) {
if(typeof child === "string") {
child = document.createTextNode(child)
}
e.appendChild(child);
}
return e;
}
运行代码,控制台打印错误:
Uncaught TypeError: e.setAttribute is not a function
很显然,问题是由我们自定义的节点带来的,让我们对代码做进一步的拆分。
首先,我们需要对不同类型的节点进行不同的处理:
定义ElementWrapper和TextWrapper
class ElementWrapper {
constructor(type) {
this.root = document.createElement(type);
}
setAttribute(name, value) {
this.root.setAttribute(name, value)
}
appendChild(component) {
this.root.appendChild(component.root)
}
}
class TextWrapper {
constructor(content) {
this.root = document.createTextNode(content);
}
}
对于自定义的component,我们则进行如下的处理:
export class Component {
constructor() {
this.props = Object.create(null);
this.children = [];
this._root = null;
}
setAttribute(name, value) {
this.props[name] = value;
}
appendChild(component) {
this.children.push(component);
}
get root() {
if (!this._root) {
this._root = this.render().root;
}
return this._root;
}
}
这里注意到root
方法,在这里,若没有root则执行一次render函数,会循环调用render函数,直到找到root为止。
我们让MyComponent继承Component:
class MyComponent extends Component {
render() {
return <div>
<h1>my component</h1>
{this.children}
</div>
}
}
此时createElement尚未进行多层子节点嵌套的处理,因此需要改进createElement函数:
export function createElement(type, attributes, ...children) {
let e;
if (typeof type === "string") {
e = new ElementWrapper(type);
} else {
e = new type;
}
for (let p in attributes) {
e.setAttribute(p, attributes[p]);
}
let insertChildren = (children) => {
for (let child of children) {
if(typeof child === "string") {
child = new TextWrapper(child)
}
if(typeof child == "object" && (child instanceof Array)) {
insertChildren(child);
} else {
e.appendChild(child);
}
}
}
insertChildren(children);
return e;
}
至此,react对jsx生成实节点的基本处理就完成了。
为Toy-React添加生命周期
让组件拥有重新渲染的能力
我们都知道react将组件的状态放在了state当中,那么在toy-react实现的过程中,难的不是为组件添加state属性,难的是如何处理setState方法。我们首先来为组件添加state属性:
class MyComponent extends Component {
constructor() {
super();
this.state = {
a: 1,
b: 2
}
}
render() {
return <div>
<h1>my component</h1>
<span>{this.state.a.toString()}</span>
{this.children}
</div>
}
}
修改MyComponent实现的代码,我们能很轻易地获取到组件的state内容,并将它渲染到页面上。
那么接下来,我们就迎来了难点如何去实现state的更新,组件在更新的操做一定是在render中实现的。我们上一阶段的代码,是在root中实现对render的调用的,我们在Component类中添加了get root
的方法:
get root() {
if (!this._root) {
this._root = this.render().root;
}
return this._root;
}
但是现在我们没有办法达到通过root实现更新的目的,所以在Component类中我们加入新的方法Render_To_DOM
,这里会用到range,不熟悉range的小伙伴可以参考相关的MDN文档:https://developer.mozilla.org/zh-CN/docs/Web/API/Range,我们接着往下走:
const RENDER_TO_DOM = Symbol("render to dom");
[RENDER_TO_DOM](range){
this.render()[RENDER_TO_DOM](range);
}
同样的,为 ElementWrapper
和TextWrapper
也添加Render_To_Dom
方法:
[RENDER_TO_DOM](range){
range.deleteContents();
range.insertNode(this.root);
}
更改render方法的内容:
export function render(component, parentElement) {
let range = document.createRange();
range.setStart(parentElement, 0);
range.setEnd(parentElement, parentElement.childNodes.length);
range.deleteContents();
component[RENDER_TO_DOM](range);
}
对 ElementWrapper
的appendChild
也进行更改
appendChild(component) {
let range = document.createRange();
range.setStart(this.root, this.root.childNodes.length);
range.setEnd(this.root, this.root.childNodes.length);
component[RENDER_TO_DOM](range);
}
这样我们就拥有了重新渲染的能力,附上更改后的全部代码:
const RENDER_TO_DOM = Symbol("render to dom");
class ElementWrapper {
constructor(type) {
this.root = document.createElement(type);
}
setAttribute(name, value) {
this.root.setAttribute(name, value)
}
appendChild(component) {
let range = document.createRange();
range.setStart(this.root, this.root.childNodes.length);
range.setEnd(this.root, this.root.childNodes.length);
component[RENDER_TO_DOM](range);
}
[RENDER_TO_DOM](range){
range.deleteContents();
range.insertNode(this.root);
}
}
class TextWrapper {
constructor(content) {
this.root = document.createTextNode(content);
}
[RENDER_TO_DOM](range){
this.render()[RENDER_TO_DOM](range);
}
[RENDER_TO_DOM](range){
range.deleteContents();
range.insertNode(this.root);
}
}
export class Component {
constructor() {
this.props = Object.create(null);
this.children = [];
this._root = null;
}
setAttribute(name, value) {
this.props[name] = value;
}
appendChild(component) {
this.children.push(component);
}
[RENDER_TO_DOM](range){
this.render()[RENDER_TO_DOM](range);
}
}
export function createElement(type, attributes, ...children) {
let e;
if (typeof type === "string") {
e = new ElementWrapper(type);
} else {
e = new type;
}
for (let p in attributes) {
e.setAttribute(p, attributes[p]);
}
let insertChildren = (children) => {
for (let child of children) {
if(typeof child === "string") {
child = new TextWrapper(child)
}
if(typeof child == "object" && (child instanceof Array)) {
insertChildren(child);
} else {
e.appendChild(child);
}
}
}
insertChildren(children);
return e;
}
export function render(component, parentElement) {
let range = document.createRange();
range.setStart(parentElement, 0);
range.setEnd(parentElement, parentElement.childNodes.length);
range.deleteContents();
component[RENDER_TO_DOM](range);
}
组件进行重新渲染功能实现
首先如果是重新绘制的话,我们需要将刚才的range做一个存储,以component为例:
export class Component {
constructor() {
this.props = Object.create(null);
this.children = [];
this._root = null;
this._range = null;
}
setAttribute(name, value) {
this.props[name] = value;
}
appendChild(component) {
this.children.push(component);
}
[RENDER_TO_DOM](range){
this._range = range;
this.render()[RENDER_TO_DOM](range);
}
rerender() {
this._range.deleteContents();
this[RENDER_TO_DOM](this._range);
}
}
将range保存在this._range
当中,并且定义了它的rerender
方法,更改main.js
文件进行测试,在render中加入一个点击事件:
render() {
return <div>
<h1>my component</h1>
<button onclick={()=> {this.state.a ++; this.rerender();}}>add</button>
<span>{this.state.a.toString()}</span>
</div>
}
此时需要更改ElementWrapper
对点击事件进行单独处理,修改ElementWrapper
的setAttribute
函数
setAttribute(name, value) {
if(name.match(/^on([\s\S]+)$/)) {
this.root.addEventListener(RegExp.$1.replace(/^[\s\S]/, c => c.toLowerCase()), value);
} else {
this.root.setAttribute(name, value);
}
}
此时点击button
即可完成页面的重新渲染。
setState的api实现
我们知道在react中,setState会把旧的state和新的state合并起来,不会把旧的完全替换掉。而且rerender是不需要手动去调用的,当我们调用setState方法后,会自动触发重新的渲染。
下面来看setState的方法:
setState(newState) {
if (this.state === null || typeof this.state !== "object") {
this.state = newState;
this.rerender();
return;
}
let merge = (oldState, newState) => {
for (let p in newState) {
//js著名的坑,因为null的typeof也是object类型的
if(oldState[p] === null || typeof oldState[p] !== "object") {
oldState[p] = newState[p];
} else {
merge(oldState[p], newState[p]);
}
}
}
merge(this.state, newState);
this.rerender();
}
对应的进行测试,修改main中的render方法:
render() {
return <div>
<h1>my component</h1>
<button onclick={()=> {this.setSate({a: this.state.a + 1})}}>add</button>
<span>{this.state.a.toString()}</span>
<span>{this.state.a.toString()}</span>
</div>
}
这样就完成了setState的渲染。
Tic-Tac-Toe试运行
我们将tic-tac-toe的阶段性结果的代码粘贴到自己的代码上,尝试用写的toy-react来运行tic-tac-toe,此处为tic-tac-toy代码:https://codepen.io/gaearon/pen/VbbVLg?editors=0010
粘贴后main.html页面为:
<style>
body {
font: 14px "Century Gothic", Futura, sans-serif;
margin: 20px;
}
ol, ul {
padding-left: 30px;
}
.board-row:after {
clear: both;
content: "";
display: table;
}
.status {
margin-bottom: 10px;
}
.square {
background: #fff;
border: 1px solid #999;
float: left;
font-size: 24px;
font-weight: bold;
line-height: 34px;
height: 34px;
margin-right: -1px;
margin-top: -1px;
padding: 0;
text-align: center;
width: 34px;
}
.square:focus {
outline: none;
}
.kbd-navigation .square:focus {
background: #ddd;
}
.game {
display: flex;
flex-direction: row;
}
.game-info {
margin-left: 20px;
}
</style>
<body>
<div id="root"></div>
</body>
<script src="main.js"></script>
main.js页面为:
import {createElement, Component, render} from "./toy-react.js"
class Square extends Component {
constructor(props) {
super(props);
this.state = {
value: null,
};
}
render() {
return (
<button
className="square"
onClick={() => this.setState({value: 'X'})}
>
{this.state.value}
</button>
);
}
}
class Board extends Component {
renderSquare(i) {
return <Square />;
}
render() {
const status = 'Next player: X';
return (
<div>
<div className="status">{status}</div>
<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</div>
</div>
);
}
}
class Game extends Component {
render() {
return (
<div className="game">
<div className="game-board">
<Board />
</div>
<div className="game-info">
<div>{/* status */}</div>
<ol>{/* TODO */}</ol>
</div>
</div>
);
}
}
// ========================================
render(
<Game />,
document.getElementById('root')
);
页面运行后报错:
toy-react.js:46 Uncaught TypeError: Cannot read property 'Symbol(render to dom)' of null
所以需要优化toy-react.js中的代码,在createElement
的insertChildren
加入child
为null
的判断:
let insertChildren = (children) => {
for (let child of children) {
if(typeof child === "string") {
child = new TextWrapper(child)
}
if(child === null) {
continue;
}
if(typeof child == "object" && (child instanceof Array)) {
insertChildren(child);
} else {
e.appendChild(child);
}
}
}
这样就可以运行了,但是有一个问题是页面的样式好像没有生效,检查以后发现是我们的toy-react中没有加入对className的处理:
setAttribute(name, value) {
if(name.match(/^on([\s\S]+)$/)) {
//大小写敏感事件若采用驼峰命名则单独处理
this.root.addEventListener(RegExp.$1.replace(/^[\s\S]/, c => c.toLowerCase()), value);
} else {
if(name == "className") {
this.root.setAttribute("class", value);
} else {
this.root.setAttribute(name, value);
}
}
}
这样游戏就能正常地运行起来了。
此时会产生一个新的问题,当我们从左往右进行点击的时候,页面就会丢东西,这个问题是由range带来的。
我们看一下rerender的代码:
rerender() {
this._range.deleteContents();
this[RENDER_TO_DOM](this._range);
}
当我们调用deleteContents
时,会让range成为一个全空的range,这时会导致一个问题,就是相邻的range会把这个全空的range吞进去,我们再做插入的时候就会被后边的range包含进去。所以我们再rerender的时候要保证这个range是不空的,为了保证range不空,我们就要先插入,再删除:
rerender() {
//保存老的range,避免调用RENDER_TO_DOM方法插入后修改了this._range
let oldRange = this._range;
let range = document.createRange();
range.setStart(_this.range.startContainer, _this.range.startOffset);
range.setEnd(_this.range.startContainer, _this.range.startOffset);
this[RENDER_TO_DOM](range);
//将老的range挪到插入之后
oldRange.setStart(range.endContainer, range.endOffset);
oldRange.deleteContents();
}
然后我们把最终的tic-tac-toe的代码放到页面中:
页面代码:https://codepen.io/gaearon/pen/gWWZgR?editors=0010
当然,代码要根据我们的实现做一个微调,挪动后的main.js:
import {createElement, Component, render} from "./toy-react.js"
class Square extends Component {
render() {
return (
<button className="square" onClick={this.props.onClick}>
{this.props.value}
</button>
);
}
}
class Board extends Component {
renderSquare(i) {
return (
<Square
value={this.props.squares[i]}
onClick={() => this.props.onClick(i)}
/>
);
}
render() {
return (
<div>
<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</div>
</div>
);
}
}
class Game extends Component {
constructor(props) {
super(props);
this.state = {
history: [
{
squares: Array(9).fill(null)
}
],
stepNumber: 0,
xIsNext: true
};
}
handleClick(i) {
const history = this.state.history.slice(0, this.state.stepNumber + 1);
const current = history[history.length - 1];
const squares = current.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? "X" : "O";
this.setState({
history: history.concat([
{
squares: squares
}
]),
stepNumber: history.length,
xIsNext: !this.state.xIsNext
});
}
jumpTo(step) {
this.setState({
stepNumber: step,
xIsNext: (step % 2) === 0
});
}
render() {
const history = this.state.history;
const current = history[this.state.stepNumber];
const winner = calculateWinner(current.squares);
const moves = history.map((step, move) => {
const desc = move ?
'Go to move #' + move :
'Go to game start';
return (
<li key={move}>
<button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
});
let status;
if (winner) {
status = "Winner: " + winner;
} else {
status = "Next player: " + (this.state.xIsNext ? "X" : "O");
}
return (
<div className="game">
<div className="game-board">
<Board
squares={current.squares}
onClick={i => this.handleClick(i)}
/>
</div>
<div className="game-info">
<div>{status}</div>
<ol>{moves}</ol>
</div>
</div>
);
}
}
// ========================================
render(<Game />, document.getElementById("root"));
function calculateWinner(squares) {
const lines = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6]
];
for (let i = 0; i < lines.length; i++) {
const [a, b, c] = lines[i];
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
return squares[a];
}
}
return null;
}
成功运行。
虚拟DOM原理和关键实现
在此之前我们已经实现了react的基本功能,但是它有一个致命的问题,没有实现虚拟DOM,这将导致我们每次进行的更新都是全量的。在这样的情况下,哪怕我们是一个很细小的操作,都会导致页面的整体的dom的更新,这是完全不可接受的,所以我们在这里加入了vdom的概念。
虚拟DOM树的构建
我们在ElementWrapper
中加入方法:
get vdom(){
return {
type: this.type,
props: this.props,
children: this.children.map(child => child.vdom)
}
}
其中this.type
可以在contructor当中进行获得,那么props和children呢?我们知道这两个其实是与ElementWrapper
的setAttribute
和appendChild
相关的(一个是存this.props
另一个是存this.children
)我们就要对这两个模块的代码做一定的改造。而这个逻辑就是Component
里面的逻辑,我们让ElementWrapper
去extendsComponent
。
同理,对TextWrapper
也这么做,在TextWrapper
中加入vdom方法:
get vdom() {
return {
type:"#text",
content: this.content
}
}
实现Component
的vdom方法:
get vdom() {
return this.render().vdom;
}
修改main.js代码,把vdom做一个打印输出:
// render(<Game />, document.getElementById("root"));
let game = <Game/>;
console.log(game.vdom);
结果查看:
我们可以看到这个div是没有children的,我们必须要把ElementWrapper
中的setAttribute
和appendChild
方法注释掉,它们才能调用到Component中的方法。
注释掉以后再打印,可以弹道children和prop就出来了:
到这一步为止我们已经完成了虚拟dom树的构建。
虚拟DOM生成真实的DOM
因为我们是基于vdom去做一个更新的,所以ElementWrapper
中的constructor方法中的root就不再需要了。接下来看获得vdom的方法,现在这个方法返回的是一个新的对象,这个返回其实是有问题的,如果这个对象上面没有方法,我们就没办法完成重绘,所以我们更改这个vdom的方法。
将vdom的get方法都改为return this;
这时vom的children变成了组件的children,我们为Component加入vchildren的方法:
get vchildren() {
return this.children.map(child => child.vdom);
}
接下来,在原来的ElementWrappe
中setAttribute
和appendChild
方法做的事情,我们要放到RENDER_TO_DOM
方法里面去完成,更改RENDER_TO_DOM
方法:
[RENDER_TO_DOM](range){
range.deleteContents();
let root = document.createElement(this.type);
//所有prop里面的内容要抄写到attribute上
for(let name in this.props) {
let value = this.props[name];
if(name.match(/^on([\s\S]+)$/)) {
//大小写敏感事件若采用驼峰命名则单独处理
root.addEventListener(RegExp.$1.replace(/^[\s\S]/, c => c.toLowerCase()), value);
} else {
if(name == "className") {
root.setAttribute("class", value);
} else {
root.setAttribute(name, value);
}
}
}
//处理children
for (let child of this.children) {
let childRange = document.createRange();
childRange.setStart(root, root.childNodes.length);
childRange.setEnd(root, root.childNodes.length);
child[RENDER_TO_DOM](childRange);
}
range.insertNode(root);
}
到这一步我们已经实现了虚拟dom到实体dom的更新。
VDOM比对
接下来我们就开始进行重头戏,vdom树的比对了。
首先我们得rerender方法可以退休了,我们不再是实现重新渲染,而是更新操作。而vdom的比对,我们将放在Component
里面去实现,首先看一下RENDER_TO_DOM
的方法,因为我们要在里面完成节点的渲染,所以我们的比对也需要在里面进行:
我们创建update方法只会进行同位置的节点的比对:
update() {
let isSameNode = (oldNode, newNode) => {
if (oldNode.type != newNode.type)
return false;
for (let name in newNode.props) {
if (newNode.props[name] !== oldNode.props[name]) {
return false;
}
}
if (Object.keys(oldNode.props).length > Object.keys(newNode.props).length)
return false;
if (newNode.type === "#text") {
if(newNode.content !== oldNode.content)
return false;
}
return true;
}
//递归访问vdom的内容
let update = (oldNode, newNode) => {
//type, props, children
//#text content
if(!isSameNode(oldNode, newNode)) {
newNode[RENDER_TO_DOM](oldNode._range);
return;
}
newNode._range = oldNode._range;
//处理children的问题
let newChildren = newNode.vchildren;
let oldChildren = oldNode.vchildren;
for (let i = 0; i < newChildren.length; i++) {
let newChild = newChildren[i];
let oldChild = oldChildren[i];
if (i < oldChildren.length) {
update(oldChild, newChild);
} else {
//TODO
}
}
}
let vdom = this.vdom;
update(this._vdom, vdom);
this._vdom = vdom;
}
单独抽调replace方法进行range的更新
function replaceContent(range, node) {
range.insertNode(node);
range.setStartAfter(node);
range.deleteContents();
range.setStartBefore(node);
range.setEndAfter(node);
}
将 TODO
代码补全
let tailRange = oldChildren[oldChildren.length - 1]._range;
for (let i = 0; i < newChildren.length; i++) {
let newChild = newChildren[i];
let oldChild = oldChildren[i];
if (i < oldChildren.length) {
update(oldChild, newChild);
} else {
//如果oldchildre的数量小于newchildren的数量,我们就要去执行插入
let range = document.createRange();
range.setStart(tailRange.endContainer, tailRange.endOffseet);
range.setEnd(tailRange.endContainer, tailRange.endOffseet);
newChild[RENDER_TO_DOM](range);
tailRange = range;
}
}
最终代码
在完成一些小的bug的修复后,我们就实现了一个基本的toy-react代码:
const RENDER_TO_DOM = Symbol("render to dom");
export class Component {
constructor() {
this.props = Object.create(null);
this.children = [];
this._root = null;
this._range = null;
}
setAttribute(name, value) {
this.props[name] = value;
}
appendChild(component) {
this.children.push(component);
}
get vdom() {
return this.render().vdom;
}
[RENDER_TO_DOM](range){
this._range = range;
//赋值的vdom是一个getter,将会重新render得到一棵新的dom树
this._vdom = this.vdom;
this._vdom[RENDER_TO_DOM](range);
}
update() {
let isSameNode = (oldNode, newNode) => {
if (oldNode.type != newNode.type)
return false;
for (let name in newNode.props) {
if (newNode.props[name] !== oldNode.props[name]) {
return false;
}
}
if (Object.keys(oldNode.props).length > Object.keys(newNode.props).length)
return false;
if (newNode.type === "#text") {
if(newNode.content !== oldNode.content)
return false;
}
return true;
}
//递归访问vdom的内容
let update = (oldNode, newNode) => {
//type, props, children
//#text content
if(!isSameNode(oldNode, newNode)) {
newNode[RENDER_TO_DOM](oldNode._range);
return;
}
newNode._range = oldNode._range;
//处理children的问题
let newChildren = newNode.vchildren;
let oldChildren = oldNode.vchildren;
if(!newChildren || !newChildren.length) {
return;
}
let tailRange = oldChildren[oldChildren.length - 1]._range;
for (let i = 0; i < newChildren.length; i++) {
let newChild = newChildren[i];
let oldChild = oldChildren[i];
if (i < oldChildren.length) {
update(oldChild, newChild);
} else {
//如果oldchildre的数量小于newchildren的数量,我们就要去执行插入
let range = document.createRange();
range.setStart(tailRange.endContainer, tailRange.endOffseet);
range.setEnd(tailRange.endContainer, tailRange.endOffseet);
newChild[RENDER_TO_DOM](range);
tailRange = range;
}
}
}
let vdom = this.vdom;
update(this._vdom, vdom);
this._vdom = vdom;
}
// rerender() {
// //保存老的range,避免调用RENDER_TO_DOM方法插入后修改了this._range
// let oldRange = this._range;
// let range = document.createRange();
// range.setStart(this._range.startContainer, this._range.startOffset);
// range.setEnd(this._range.startContainer, this._range.startOffset);
// this[RENDER_TO_DOM](range);
// //将老的range挪到插入之后
// oldRange.setStart(range.endContainer, range.endOffset);
// oldRange.deleteContents();
// }
setState(newState) {
if (this.state === null || typeof this.state !== "object") {
this.state = newState;
this.update();
return;
}
let merge = (oldState, newState) => {
for (let p in newState) {
//js著名的坑,因为null的typeof也是object类型的
if(oldState[p] === null || typeof oldState[p] !== "object") {
oldState[p] = newState[p];
} else {
merge(oldState[p], newState[p]);
}
}
}
merge(this.state, newState);
this.update();
}
}
class ElementWrapper extends Component{
constructor(type) {
super(type)
this.type = type;
}
// setAttribute(name, value) {
// if(name.match(/^on([\s\S]+)$/)) {
// //大小写敏感事件若采用驼峰命名则单独处理
// this.root.addEventListener(RegExp.$1.replace(/^[\s\S]/, c => c.toLowerCase()), value);
// } else {
// if(name == "className") {
// this.root.setAttribute("class", value);
// } else {
// this.root.setAttribute(name, value);
// }
// }
// }
get vdom(){
this.vchildren = this.children.map(child => child.vdom);
return this;
// {
// type: this.type,
// props: this.props,
// children: this.children.map(child => child.vdom)
// }
}
// appendChild(component) {
// let range = document.createRange();
// range.setStart(this.root, this.root.childNodes.length);
// range.setEnd(this.root, this.root.childNodes.length);
// component[RENDER_TO_DOM](range);
// }
[RENDER_TO_DOM](range){
this._range = range;
let root = document.createElement(this.type);
//所有prop里面的内容要抄写到attribute上
for(let name in this.props) {
let value = this.props[name];
if(name.match(/^on([\s\S]+)$/)) {
//大小写敏感事件若采用驼峰命名则单独处理
root.addEventListener(RegExp.$1.replace(/^[\s\S]/, c => c.toLowerCase()), value);
} else {
if(name == "className") {
root.setAttribute("class", value);
} else {
root.setAttribute(name, value);
}
}
}
if(!this.vchildren) {
this.vchildren = this.children.map(child => child.vdom);
}
//处理children
for (let child of this.vchildren) {
let childRange = document.createRange();
childRange.setStart(root, root.childNodes.length);
childRange.setEnd(root, root.childNodes.length);
child[RENDER_TO_DOM](childRange);
}
replaceContent(range, root)
}
}
class TextWrapper extends Component {
constructor(content) {
super(content);
this.type = "#text";
this.content = content;
}
get vdom() {
return this;
// {
// type:"#text",
// content: this.content
// }
}
[RENDER_TO_DOM](range){
this._range = range;
let root = document.createTextNode(this.content)
replaceContent(range, root)
}
}
function replaceContent(range, node) {
range.insertNode(node);
range.setStartAfter(node);
range.deleteContents();
range.setStartBefore(node);
range.setEndAfter(node);
}
export function createElement(type, attributes, ...children) {
let e;
if (typeof type === "string") {
e = new ElementWrapper(type);
} else {
e = new type;
}
for (let p in attributes) {
e.setAttribute(p, attributes[p]);
}
let insertChildren = (children) => {
for (let child of children) {
if(typeof child === "string") {
child = new TextWrapper(child)
}
if(child === null) {
continue;
}
if(typeof child == "object" && (child instanceof Array)) {
insertChildren(child);
} else {
e.appendChild(child);
}
}
}
insertChildren(children);
return e;
}
export function render(component, parentElement) {
let range = document.createRange();
range.setStart(parentElement, 0);
range.setEnd(parentElement, parentElement.childNodes.length);
range.deleteContents();
component[RENDER_TO_DOM](range);
}