React是前端最受欢迎的框架之一,解读其源码的文章很是多,可是我想从另外一个角度去解读React:从零开始实现一个React,从API层面实现React的大部分功能,在这个过程当中去探索为何有虚拟DOM、diff、为何setState这样设计等问题。html
提起React,老是免不了和Vue作一番对比前端
Vue的API设计很是简洁,可是其实现方式却让人感受是“魔法”,开发者虽然能立刻上手,可是为何能实现功能却很难说清楚。node
相比之下React的设计哲学很是简单,虽然常常有须要本身处理各类细节问题,可是却让人感受它很是“真实”,能清楚地感受到本身仍然是在写js。react
在开始以前,咱们有必要搞清楚一些概念。git
咱们来看一下这样一段代码:es6
const title = <h1 className="title">Hello, world!</h1>;
复制代码
这段代码并非合法的js代码,它是一种被称为jsx的语法扩展,经过它咱们就能够很方便的在js代码中书写html片断。github
本质上,jsx是语法糖,上面这段代码会被babel转换成以下代码npm
const title = React.createElement(
'h1',
{ className: 'title' },
'Hello, world!'
);
复制代码
你能够在babel官网提供的在线转译测试jsx转换后的代码,这里有一个稍微复杂一点的例子json
为了集中精力编写逻辑,在代码打包工具上选择了最近火热的零配置打包工具parcel,须要先安装parcel:数组
npm install -g parcel-bundler
复制代码
接下来新建index.js
和index.html
,在index.html
中引入index.js
。
固然,有一个更简单的方法,你能够直接下载这个仓库的代码:
https://github.com/hujiulong/simple-react/tree/chapter-1
注意一下babel的配置 .babelrc
{
"presets": ["env"],
"plugins": [
["transform-react-jsx", {
"pragma": "React.createElement"
}]
]
}
复制代码
这个transform-react-jsx
就是将jsx转换成js的babel插件,它有一个pragma
项,能够定义jsx转换方法的名称,你也能够将它改为h
(这是不少类React框架使用的名称)或别的。
准备工做完成后,咱们能够用命令parcel index.html
将它跑起来了,固然,如今它还什么都没有。
前文提到,jsx片断会被转译成用React.createElement
方法包裹的代码。因此第一步,咱们来实现这个React.createElement
方法
从jsx转译结果来看,createElement方法的参数是这样:
createElement( tag, attrs, child1, child2, child3 );
复制代码
第一个参数是DOM节点的标签名,它的值多是div
,h1
,span
等等 第二个参数是一个对象,里面包含了全部的属性,可能包含了className
,id
等等 从第三个参数开始,就是它的子节点
咱们对createElement的实现很是简单,只须要返回一个对象来保存它的信息就好了。
function createElement( tag, attrs, ...children ) {
return {
tag,
attrs,
children
}
}
复制代码
函数的参数...children
使用了ES6的rest参数,它的做用是将后面child1,child2等参数合并成一个数组children。
如今咱们来试试调用它
// 将上文定义的createElement方法放到对象React中
const React = {
createElement
}
const element = (
<div> hello<span>world!</span> </div>
);
console.log( element );
复制代码
打开调试工具,咱们能够看到输出的对象和咱们预想的一致
咱们的createElement方法返回的对象记录了这个DOM节点全部的信息,换言之,经过它咱们就能够生成真正的DOM,这个记录信息的对象咱们称之为虚拟DOM。
接下来是ReactDOM.render方法,咱们再来看这段代码
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('root')
);
复制代码
通过转换,这段代码变成了这样
ReactDOM.render(
React.createElement( 'h1', null, 'Hello, world!' ),
document.getElementById('root')
);
复制代码
因此render
的第一个参数实际上接受的是createElement返回的对象,也就是虚拟DOM 而第二个参数则是挂载的目标DOM
总而言之,render方法的做用就是将虚拟DOM渲染成真实的DOM,下面是它的实现:
function render( vnode, container ) {
// 当vnode为字符串时,渲染结果是一段文本
if ( typeof vnode === 'string' ) {
const textNode = document.createTextNode( vnode );
return container.appendChild( textNode );
}
const dom = document.createElement( vnode.tag );
if ( vnode.attrs ) {
Object.keys( vnode.attrs ).forEach( key => {
if ( key === 'className' ) key = 'class'; // 当属性名为className时,改回class
dom.setAttribute( key, vnode.attrs[ key ] )
} );
}
vnode.children.forEach( child => render( child, dom ) ); // 递归渲染子节点
return container.appendChild( dom ); // 将渲染结果挂载到真正的DOM上
}
复制代码
这里注意React为了不类名class
和js关键字class
冲突,将类名改为了className,在渲染成真实DOM时,须要将其改回。
这里其实还有个小问题:当屡次调用render
函数时,不会清除原来的内容。因此咱们将其附加到ReactDOM对象上时,先清除一下挂载目标DOM的内容:
const ReactDOM = {
render: ( vnode, container ) => {
container.innerHTML = '';
return render( vnode, container );
}
}
复制代码
到这里咱们已经实现了React最为基础的功能,能够用它来作一些事了。
咱们先在index.html中添加一个根节点
<div id="root"></div>
复制代码
咱们先来试试官方文档中的Hello,World
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('root')
);
复制代码
能够看到结果:
试试渲染一段动态的代码,这个例子也来自官方文档
function tick() {
const element = (
<div> <h1>Hello, world!</h1> <h2>It is {new Date().toLocaleTimeString()}.</h2> </div>
);
ReactDOM.render(
element,
document.getElementById( 'root' )
);
}
setInterval( tick, 1000 );
复制代码
能够看到结果:
这篇文章中,咱们实现了React很是基础的功能,也了解了jsx和虚拟DOM,下一篇文章咱们将实现很是重要的组件功能。
最后留下一个小问题 在定义React组件或者书写React相关代码,无论代码中有没有用到React这个对象,咱们都必须将其import进来,这是为何?
例如:
import React from 'react'; // 下面的代码没有用到React对象,为何也要将其import进来
import ReactDOM from 'react-dom';
ReactDOM.render( <App />, document.getElementById( 'editor' ) ); 复制代码
不知道答案的同窗再仔细看看这篇文章哦
React是前端最受欢迎的框架之一,解读其源码的文章很是多,可是我想从另外一个角度去解读React:从零开始实现一个React,从API层面实现React的大部分功能,在这个过程当中去探索为何有虚拟DOM、diff、为何setState这样设计等问题。
整个系列大概会有六篇左右,我每周会更新一到两篇,我会第一时间在github上更新,有问题须要探讨也请在github上回复我~
博客地址: github.com/hujiulong/b… 关注点star,订阅点watch