准备工作
git地址 mini-react
如果不能使用请联系我。
这里不做太多 工具的介绍,所以打包工具选用相对简单的parcel。
npm install -g parcel-bundler
接下来新建index.js
和index.html
,在index.html
中引入index.js
。
然后就是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中render或者函数组件中返回的代码,如下:
const title = Hello, world!
;
这段代码并不是合法的js代码,它是一种被称为jsx的语法扩展,通过它我们就可以很方便的在js代码中书写html片段。
本质上,jsx是语法糖,上面这段代码会被babel转换成如下代码
const title = React.createElement(
'h1',
{ className: 'title' },
'Hello, world!'
);
有兴趣,可以用babel做个测试 babel测试
React.createElement和虚拟DOM
我们知道所有的jsx
代码,都会被转换成React.createElement
这种形式
从jsx解析结果来看,createElement方法参数应该是:
createElement(tag,attrs,child1,child2,child3,child4)
第一个参数是DOM元素的标签名,比如说:div,span,h1,main
第二个参数是一个对象,里面包含了所有属性,可能包含className,id等等
第三个参数开始,就是它的子节点。
那我们只要自己一个React
全局对象,给它挂载这个React.createElement
方法就可以进行接下来的处理:
const React = {};
React.createElement = function(tag, attrs, ...children) {
return {
tag,
attrs,
children
};
};
export default React;
我们的createElement方法返回的对象记录了这个DOM节点所有信息,换句话说,通过它我们就可以生成真正的DOM,这个记录信息的对象我们称之为虚拟DOM。
接下来来测试下:
打开调试工具,我们可以看到输出的对象和我们预想的一致
处理好了jsx,现在我们来开始处理入口
ReactDOM.render
方法是我们的入口
先定义ReactDOM
对象,然后看它的render
方法~
先看看默认解析是什么样
const ReactDOM={};
ReactDOM.render(
Hello, world!
,
document.getElementById('root')
);
//经过转换
ReactDOM.render(
React.createElement( 'h1', null, 'Hello, world!' ),
document.getElementById('root')
);
可以看到ReactDom.render的解析,所以render第一个参数是createElement返回的对象,也就是虚拟DOM,,第二个参数也就是要挂载目标的DOM。
也就是说render方法就把虚拟dom
对象-js
对象变成真实dom
对象,然后插入到根标签内。
然后开始定义:
const ReactDom = {};
const render = function(vnode, container) {
return container.appendChild(_render(vnode));
};
ReactDom.render = render;
思路: 先把虚拟dom
对象-js
对象变成真实dom
对象,然后插入到根标签内。
_render
方法,接受虚拟dom
对象,返回真实dom
对象:
如果传入的是null,字符串或者数字 那么直接转换成真实dom
然后返回就可以了~,如果是其他类型要做一些特殊处理,看代码把。
import handleAttrs from './handleAttrs';
import {createComponent,setComponentProps} from '../components/utills';
const ReactDom = {};
//传入虚拟dom节点和真实包裹节点,把虚拟dom节点通过_render方法转换成真实dom节点,然后插入到包裹节点中,这个就是react的初次渲染
const render = function (vnode, container) {
return container.appendChild(_render(vnode));
};
ReactDom.render = render;
export function _render(vnode) {
console.log('reactDom render', vnode);
if (vnode === undefined || vnode === null || typeof vnode === 'boolean') {//做个防错
vnode = '';
}
if (typeof vnode === 'number') {
vnode = String(vnode);
}
if (typeof vnode === 'string') {//如果是最里面一层或者就是个字符串的话,转化成Node类型,遵从appendChild要求
let textNode = document.createTextNode(vnode);
return textNode;
}
if (typeof vnode.tag === 'function') {//是一个组件 这种 babel会给我们做转换 vnode.tag得类型
//hooks
const component = createComponent(vnode.tag, vnode.attrs);//组件类型的话,创建组件
setComponentProps(component, vnode.attrs);//设置组件属性,并且转换为真实dom,component.base里存着真实dom
return component.base;
}
// vnode= {tag,props,children}
// {tag:"li",attrs:{xxx:},children:1} 这几种情况都排除之后,那就是html元素了
const dom = document.createElement(vnode.tag);
if (vnode.attrs) {//但是有可能传入的是个div标签,而且它有属性。那么需要处理属性,由于这个处理属性的函数需要大量复用,我们单独定义成一个函数:
Object.keys(vnode.attrs).forEach(key => {
const value = vnode.attrs[key];
handleAttrs(dom, key, value);//如果有属性,例如style标签、onClick事件等,都会通过这个函数,挂在到dom上
});
}
vnode.children && vnode.children.forEach(child => render(child, dom)); // 有子集的话,递归渲染子节点
return dom;
}
export default ReactDom;
但是可能有子节点(babel转的结构,可以下代码输出看下,完事会把git链接放上来)的嵌套,于是要用到递归(就在上面代码得结尾):
vnode.children && vnode.children.forEach(child => render(child, dom));
如果要是html元素标签,而且它有属性。那么需要处理属性,由于这个处理属性的函数需要大量复用,我们单独定义成一个函数(上面handleAttrs就是这个setAttribute得导出):
export default 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);
}
}
}
然后处理上面说的组件类型了,加入新一个新的处理方式:
我们先定义好Component
这个类,并且挂载到全局React
的对象上
import { enqueueSetState } from './setState';
export class Component {
constructor(props = {}) {
this.state = {};
this.props = props;
}
setState(stateChange){
console.log('state')
enqueueSetState(newState, this);//异步合并state,并更新组件得方法
}
}
组件类型 babel也会通过React.crateElement()来给我们做转换,下面就是实现_render里所用到得createComponent创建和setComponentProps设置属性以及相关得renderComponent根据虚拟DOM生成真实DOM加载等方法。
import { Component } from './component';
export function createComponent(component, props) {
let inst;
//如果是类定义组件component是function,直接返回实例。
if (component.prototype && component.prototype.render) {
inst = new component(props);//实例化
} else {//如果是函数组件
inst = new Component(props);
inst.constructor = component; //把函数组件储存下,方便统一render调用
inst.render = function () {
this.constructor(props);//调用函数
}
}
return inst;//返回组件实例
}
export function setComponentProps(component, props) {//设置属性,并执行部分生命周期
if (!component.base) {//首次加载
if (component.componentWillMount) component.componentWillMount();//执行将要装载生命周期
} else if (component.base && component.componentWillReceiveProps) {//props变化
component.componentWillReceiveProps(props);
}
component.props = props;//props变化了,重新赋值
renderComponent(component);//生成真实dom
}
export function renderComponent(component) {
console.log('renderComponent');
let base;
//调用render方法,返回jsx,通过createElement返回虚拟dom对象 这里会用到state 此时的state已经通过上面的setState时队列合并 更新了
const renderer = component.render();
if (component.base && component.shouldComponentUpdate) {//优化用得生命周期,根据返回值确定是否继续要渲染
let result = true;
result = component.shouldComponentUpdate({}, component.newState);//props得处理有点问题,后续要改进下
if (!result) {
return;
}
}
if (component.base && component.componentWillUpdate) {//组件即将更新得时候触发生命周期函数,首次加载不触发
component.componentWillUpdate();
}
//根据diff算法,得到真实dom对象
base = diffNode(component.base, renderer);
if (component.base) {
if (component.componentDidUpdate) component.componentDidUpdate();//更新完毕生命周期,首次不加载
} else {
component.base = base;//base是真实DOM,将本次得真实DOM挂载到组件上,方便判断是否首次挂载。
base_component = component;//互相引用,方便后续队列处理
component.componentDidMount && component.componentDidMount();//首次挂载完后,真实dom生成后得生命周期
return;
}
//不是首次加载,挂载真实的dom对象到对应的 组件上 方便后期对比
component.base = base;
//不是首次加载,挂载对应到组件到真实dom上 方便后期对比~
base._component = component;
}
然后就是根据虚拟DOM得到真实DOM得 diff算法
总而言之,我们的diff算法有两个原则:
- 对比当前真实的DOM和虚拟DOM,在对比过程中直接更新真实DOM
- 只对比同一层级的变化
import handleAttrs from '../reactDom/handleAttrs';
import {setComponentProps,createComponent} from '../components/utills';
/**
* @param {HTMLElement} dom 真实DOM
* @param {vnode} vnode 虚拟DOM
* @param {HTMLElement} container 容器
* @returns {HTMLElement} 更新后的DOM
*/
export function diffNode(dom, vnode) {//当前得dom 真实DOM,当前得vnode 虚拟DOM
let out = dom;
if (vnode === undefined || vnode === null || typeof vnode === 'boolean') {
vnode = '';
}
if (typeof vnode === 'number') {
vnode = String(vnode);
}
if (typeof vnode === 'string') {//diff text node 文本节点
if (dom && dom.nodeType === 3) {//当前dom就是文本节点
if (dom.textContent !== vnode) {//如果内容和虚拟dom不一样,更改
dom.textContent = vnode;
}
} else {//如果当前dom不是文本节点,创建一个新的文本节点,并更新;
out = document.createTextNode(vnode);
if (dom && dom.parentNode) {
//https://www.w3school.com.cn/jsref/met_node_replacechild.asp
dom.parentNode.replaceChild(out, dom);
}
}
return out;//更新完,返回
}
if (typeof vnode.tag === 'function') {//因为会递归调用,如果某一次调用传入得是组件类型,也就是说调用过程中有一层得节点是组件,就对比组件更新
return diffComponent(dom, vnode);
}
console.log(dom, vnode)
if (!dom || !isSameNodeType(dom, vnode)) {//如果真实DOM要是不存在,或者当前元素标签层级有变化得话
out = document.createElement(vnode.tag);
if (dom) {
[...dom.childNodes].map(item => out.appendChild(item)); // 将原来的子节点移到新节点下
console.log(out, 'out', dom.childNodes)
if (dom.parentNode) {
dom.parentNode.replaceChild(out, dom); // 移除掉原来的DOM对象
}
}
}
if (
(vnode.children && vnode.children.length > 0) ||
(out.childNodes && out.childNodes.length > 0)
) {//如果 有子集的话,对比子集更新, 两个都要判断一种虚拟dom有子集,一种是虚拟DOM没子集,但是真实dom有子集
diffChildren(out, vnode.children);
}
diffAttributes(out, vnode);//更新属性
return out;
}
export function diffAttributes(dom, vnode) {
const old = {}; // 当前DOM的属性
const attrs = vnode.attrs; // 虚拟DOM的属性
for (let i = 0; i < dom.attributes.length; i++) {
const attr = dom.attributes[i];
old[attr.name] = attr.value;
}
// 如果原来的属性不在新的属性当中,则将其移除掉(属性值设为undefined)
for (let name in old) {
if (!(name in attrs)) {
handleAttrs(dom, name, undefined);
}
}
// 更新新的属性值
for (let name in attrs) {
if (old[name] !== attrs[name]) {
handleAttrs(dom, name, attrs[name]);
}
}
}
function diffChildren(dom, vchildren) {//父级真实dom,和虚拟DOM子集
const domChildren = dom.childNodes;//真实DOM子集 和vchildren对应 同层次对比
//没有key得真实DOM集合
const children = [];
//有key得集合
const keyed = {};
if (domChildren.length > 0) {//真实DOM有子集
for (let i = 0; i < domChildren.length; i++) {
const child = domChildren[i];
const key = child.key;
if (key) {//根据有没有key,把子集分一下
keyed[key] = child;
} else {
children.push(child);
}
}
}
if (vchildren && vchildren.length > 0) {//虚拟dom子集有
let min = 0;
let childrenLen = children.length;//无key得长度
for (let i = 0; i < vchildren.length; i++) {//循环虚拟dom
const vchild = vchildren[i];
const key = vchild.key;
let child;
if (key) {//有key
if (keyed[key]) {//从真实dom里找一下,看有没有
child = keyed[key];//储存下
keyed[key] = undefined;//清空
}
} else if (min < childrenLen) {//否则没有key,从没key里找,并且开始childrenLen不是0
for (let j = min; j < childrenLen; j++) {
let c = children[j];
if (c && isSameNodeType(c, vchild)) {//用真实DOM和虚拟DOM比对一下看是不是同一个节点类型和值相等,包括组件得比对
child = c;//是同一个找到了,存一下
children[j] = undefined;//清空下
if (j === childrenLen - 1) childrenLen--;//做下标记,这个元素找过了,下次略过这个元素min也一样
if (j === min) min++;
break;
}
}
}
child = diffNode(child, vchild);//当前这项真实DOM和虚拟DOM做一个比对,更新如果里面还有子集又会调用diffChildren,返回真实Dom
const f = domChildren[i];//获取原来真实dom集合中得当前项
console.log(child, f)
if (child && child !== f) {//如果比对完得child和当前这个f不一样
if (!f) {//如果不存在,reacrDom第一次render时,直接添加到父级里
dom.appendChild(child);
} else {//child 已经从真实dom找过一轮,并且和虚拟DOM对比生成过的了。
// dom.insertBefore(child, f);
// dom.removeChild(f);
dom.replaceChild(child, f);//替换掉
}
}
}
if (dom) {
if (childrenLen > vchildren.length) {//删除多余节点
for (let i = vchildren.length; i < childrenLen; i++) {
dom.removeChild(children[i]);
}
}
}
}
}
function isSameNodeType(dom, vnode) {//这个方法很多地方用到
if (typeof vnode === 'string' || typeof vnode === 'number') {
return dom.nodeType === 3 && dom === String(vnode); //查看是否是文本节点
}
if (typeof vnode.tag === 'string') {
return dom.nodeName.toLowerCase() === vnode.tag.toLowerCase(); //查看当前层级是否标签换了
}
return dom && dom._component && dom._component.constructor === vnode.tag;//在renderComponent做过互相引用,
//通过createComponent方法里实例化处理根据constructor判断是否是vbode.tag得实例,如果不是当前层级 组件更换了,
//diffChildren里找对应组件时会用到这里
}
function diffComponent(dom, vnode) {
let c = dom && dom._component;
let oldDom = dom;
// 如果组件类型没有变化,则重新set props
if (c && c.constructor === vnode.tag) {
setComponentProps(c, vnode.attrs);
dom = c.base;
// 如果组件类型变化,则移除掉原来组件,并渲染新的组件
} else {
if (c) {
unmountComponent(c);
oldDom = null;
}
c = createComponent(vnode.tag, vnode.attrs);
setComponentProps(c, vnode.attrs);
dom = c.base;
if (oldDom && dom !== oldDom) {
oldDom._component = null;
removeNode(oldDom);
}
}
return dom;
}
function removeNode(dom) {
if (dom && dom.parentNode) {
dom.parentNode.removeChild(dom);
}
}
这上面里的是整套得diff算法,包含了对各种类型得处理以及子集得处理,可以跟着代码仓库例子跑一跑,自己手写一下,diffChildren会稍微复杂一点。
shouldComponentUpdate中得props第一个参数有点问题。有兴趣得同学可以看看看着在代码里做一下优化
剩下就是优化setState得合成
优化使用requestAnimationFrame
实现。window.requestAnimationFrame()
告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行
import { renderComponent } from './utills';
/**
* 队列 先进先出 后进后出 ~
* @param {Array:Object} setStateQueue 抽象队列 每个元素都是一个key-value对象 key:对应的stateChange value:对应的组件
* @param {Array:Component} renderQueue 抽象需要更新的组件队列 每个元素都是Component
*/
const setStateQueue = [];
const renderQueue = [];
function defer(fn) {
//requestIdleCallback的兼容性不好,对于用户交互频繁多次合并更新来说,requestAnimation更有及时性高优先级,requestIdleCallback则适合处理可以延迟渲染的任务~
// if (window.requestIdleCallback) {
// console.log('requestIdleCallback');
// return requestIdleCallback(fn);
// }
//高优先级任务 异步的 先挂起
//告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行
return requestAnimationFrame(fn);
}
export function enqueueSetState(stateChange, component) {
//第一次进来肯定会先调用defer函数
if (setStateQueue.length === 0) {
//清空队列的办法是异步执行,下面都是同步执行的一些计算
defer(flush);
}
// setStateQueue:[{state:{a:1},component:app},{state:{a:2},component:test},{state:{a:3},component:app}]
//向队列中添加对象 key:stateChange value:component
setStateQueue.push({
stateChange,
component
});
//如果渲染队列中没有这个组件 那么添加进去
if (!renderQueue.some(item => item === component)) {
renderQueue.push(component);
}
}
function flush() {//下次重绘之前调用,合并state
let item, component;
//依次取出对象,执行
while ((item = setStateQueue.shift())) {
const { stateChange, component } = item;
// 如果没有prevState,则将当前的state作为初始的prevState
if (!component.prevState) {
component.prevState = Object.assign({}, component.state);
}
let newState;
// 如果stateChange是一个方法,也就是setState的第二种形式
if (typeof stateChange === 'function') {
newState = Object.assign(
component.state,
stateChange(component.prevState, component.props)
);
} else {
// 如果stateChange是一个对象,则直接合并到setState中
newState = Object.assign(component.state, stateChange);
}
component.newState = newState;
component.prevState = component.state;
}
//先做一个处理合并state的队列,然后把state挂载到component下面 这样下面的队列,遍历时候,能也拿到state属性
//依次取出组件,执行更新逻辑,渲染
while ((component = renderQueue.shift())) {
renderComponent(component);
}
}
有兴趣的同学,可以拉下来代码自己改改看看呦。
window.requestAnimationFrame
文章参考:
从零自己编写一个React框架 【中高级前端杀手锏级别技能】
hujiulong的博客
未完待续