想要本身实现一个React
简易版框架,并非很是难。可是你须要先了解下面这些知识点
若是你能阅读如下的文章,那么会更轻松的阅读本文章:
为了下降本文难度,构建工具选择了parcel
,欢迎加入咱们的前端交流群~ gitHub
仓库源码地址和二维码都会在最后放出来~css
DOM
?其实就是一个个的具备固定格式的JS
对象,例如:html
const obj = { tag:'div', attrs:{ className:"test" }, children:[ tag:'span', attrs:{ className:"text" }, tag:'p', attrs:{ className:"p" }, ] }
DOM
对象?AST
)js
对象
这一切都是基于
Babel
作的
babel在线编译测试
class App extends React.Component{ render(){ return <div>123</div> } }
上面这段代码 会被编译成:前端
... _createClass(App, [{ key: "render", value: function render() { return React.createElement("div", null, "123"); } }]); //省略掉一部分代码
最核心的一段jsx
代码, return <div>123</div>
被转换成了:return React.createElement("div", null, "123");
node
咱们写的jsx
代码,都会被转换成React.createElement
这种形式react
那咱们只要本身一个React
全局对象,给它挂载这个React.createElement
方法就能够进行接下来的处理:webpack
const React = {}; React.createElement = function(tag, attrs, ...children) { return { tag, attrs, children }; }; export default React;
咱们定义的React.createElement
方法也很简单,只是把对应的参数集中变成一个特定格式的对象,而后返回,再接下来进行处理~。Babel
的配置会帮咱们自动把jsx
转换成React.creatElement
的代码,参数都会默认帮咱们传好~git
构建工具咱们使用零配置的parcel
,相比webpack
来讲,更容易上手,固然对于一个把webpack
玩透了的人来讲,其实用什么都同样~
npm install -g parcel-bundler
parcel index.html
便可运行项目// .babelrc 配置 { "presets": ["env"], "plugins": [ ["transform-react-jsx", { "pragma": "React.createElement" }] ] }
jsx
代码,咱们入口开始写起:ReactDOM.render
方法是咱们的入口ReactDOM
对象,以及它的render
方法~const ReactDom = {}; //vnode 虚拟dom,即js对象 //container 即对应的根标签 包裹元素 const render = function(vnode, container) { return container.appendChild(_render(vnode)); }; ReactDom.render = render;
思路: 先把虚拟dom
对象-js
对象变成真实dom
对象,而后插入到根标签内。
_render
方法,接受虚拟dom
对象,返回真实dom
对象:github
若是传入的是null,字符串或者数字 那么直接转换成真实dom
而后返回就能够了~web
if (vnode === undefined || vnode === null || typeof vnode === 'boolean') vnode = ''; if (typeof vnode === 'number') vnode = String(vnode); if (typeof vnode === 'string') { let textNode = document.createTextNode(vnode); return textNode; } const dom = document.createElement(vnode.tag); return dom
可是有可能传入的是个div
标签,并且它有属性。那么须要处理属性,因为这个处理属性的函数须要大量复用,咱们单独定义成一个函数:算法
if (vnode.attrs) { Object.keys(vnode.attrs).forEach(key => { const value = vnode.attrs[key]; handleAttrs(dom, key, value); }); } 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); } } }
可是可能有子节点的嵌套,因而要用到递归:
vnode.children && vnode.children.forEach(child => render(child, dom)); // 递归渲染子节点
上面没有考虑到组件,只考虑到了div
或者字符串数字之类的虚拟dom
.
其实加入组件也很简单:加入新一个新的处理方式:
咱们先定义好Component
这个类,而且挂载到全局React
的对象上
export class Component { constuctor(props = {}) { this.state = {}; this.props = props; } setState(stateChange) { // 将修改合并到state console.log('setstate'); const newState = Object.assign(this.state, stateChange); console.log('state:', newState); renderComponent(this); } } .... //挂载Component类到全局React上 React.Component = Component
若是是组件,Babel
会帮咱们把第一个参数变成function
if (typeof vnode.tag === 'function') { //先建立组件 const component = createComponent(vnode.tag, vnode.attrs); //设置属性 setComponentProps(component, vnode.attrs) //返回的是真实dom对象 return component.base; }
createComponent
和setComponentProps
都是咱们本身定义的方法~后期大量复用
export function createComponent(component, props) { let inst; // 若是是类定义组件,则直接返回实例 if (component.prototype && component.prototype.render) { inst = new component(props); // 若是是函数定义组件,则将其扩展为类定义组件 } else { inst = new Component(props); inst.constructor = component; inst.render = function() { return this.constructor(props); }; } return inst; }
export function setComponentProps(component, props) { if (!component.base) { if (component.componentWillMount) component.componentWillMount(); } else if (component.base && component.componentWillReceiveProps) { component.componentWillReceiveProps(props); } component.props = props; renderComponent(component); }
renderComponent
也是咱们本身定义的方法,用来渲染组件:
export function renderComponent(component) { console.log('renderComponent'); let base; const renderer = component.render(); if (component.base && component.componentWillUpdate) { component.componentWillUpdate(); } base = _render(renderer); if (component.base) { if (component.componentDidUpdate) component.componentDidUpdate(); } else { component.base = base; component.componentDidMount && component.componentDidMount(); if (component.base && component.base.parentNode) { component.base.parentNode.replaceChild(base, component.base); } return; } if (component.base && component.base.parentNode) { component.base.parentNode.replaceChild(base, component.base); } //base是真实dom对象 //component.base是将本次渲染好的dom对象挂载到组件上,方便判断是否首次挂载 component.base = base; //互相饮用,方便后期的队列处理 base._component = component; }
最简单的版本已经完成,对应的生命简单周期作了粗糙处理,可是没有加入diff
算法和异步setState
,欢迎移步gitHub
点个star
最简单版React-无diff算法和异步state,选择master分支
diff
算法和shouldComponentUpdate
生命周期优化:没有diff算法,更新state
后是全部的节点都要更新,这样性能损耗很是大。如今咱们开始加入React
的diff
算法
首先改造renderComponent
方法
function renderComponent(component, newState = {}) { console.log('renderComponent'); //真实dom对象 let base; //虚拟dom对象 const renderer = component.render(); //component.base是为了表示是否通过初次渲染,好进行生命周期函数调用 if (component.base && component.componentWillUpdate) { component.componentWillUpdate(); } if (component.base && component.shouldComponentUpdate) { //若是组件通过了初次渲染,是更新阶段,那么能够根据这个生命周期判断是否更新 let result = true; result = component.shouldComponentUpdate && component.shouldComponentUpdate((component.props = {}), newState); if (!result) { return; } } //获得diff算法对比后的真实dom对象 base = diffNode(component.base, renderer); if (component.base) { if (component.componentDidUpdate) component.componentDidUpdate(); } else { //为了防止死循环,调用完`didMount`函数就结束。 component.base = base; base._component = component; component.componentDidMount && component.componentDidMount(); return; } component.base = base; base._component = component; }
注意,咱们是跟preact
同样,将真实dom
对象和虚拟dom
对象进行对比:
分为下面几种diff:
Node
节点diff
Component
组件diff
diff
diff
...diff
(这个最复杂)纯文本或者数字的diff
:
纯文本和数字之类的直接替换掉dom
节点的textContent
便可
diffNode(dom, vnode) { let out = dom; if (vnode === undefined || vnode === null || typeof vnode === 'boolean') vnode = ''; if (typeof vnode === 'number') vnode = String(vnode); // diff text node if (typeof vnode === 'string') { // 若是当前的DOM就是文本节点,则直接更新内容 if (dom && dom.nodeType === 3) { // nodeType: https://developer.mozilla.org/zh-CN/docs/Web/API/Node/nodeType if (dom.textContent !== vnode) { dom.textContent = vnode; } // 若是DOM不是文本节点,则新建一个文本节点DOM,并移除掉原来的 } else { out = document.createTextNode(vnode); if (dom && dom.parentNode) { dom.parentNode.replaceChild(out, dom); } } return out; }
Component
组件diff
若是不是一个类型组件直接替换掉,不然只更新属性便可
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; }
属性的diff
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]); } } }
children
的diff
function diffChildren(dom, vchildren) { const domChildren = dom.childNodes; //没有key值的真实dom集合 const children = []; //有key值的集合 const keyed = {}; if (domChildren.length > 0) { for (let i = 0; i < domChildren.length; i++) { const child = domChildren[i]; const key = child.key; if (key) { keyed[key] = child; } else { children.push(child); } } } if (vchildren && vchildren.length > 0) { let min = 0; let childrenLen = children.length; for (let i = 0; i < vchildren.length; i++) { const vchild = vchildren[i]; const key = vchild.key; let child; if (key) { if (keyed[key]) { child = keyed[key]; keyed[key] = undefined; } } else if (min < childrenLen) { for (let j = min; j < childrenLen; j++) { let c = children[j]; if (c && isSameNodeType(c, vchild)) { child = c; children[j] = undefined; if (j === childrenLen - 1) childrenLen--; if (j === min) min++; break; } } } child = diffNode(child, vchild); const f = domChildren[i]; if (child && child !== dom && child !== f) { if (!f) { dom.appendChild(child); } else if (child === f.nextSibling) { removeNode(f); } else { dom.insertBefore(child, f); } } } } }
children
的diff
这段,确实看起来不那么简单,总结两点精髓:
key
值将节点分红两个队列key
值的节点,而后对比相同类型的节点,而后进行dom
操做shouldComponentUpdate
的对比优化:
shouldComponentUpdate(nextProps, nextState) { if (nextState.test > 5) { console.log('shouldComponentUpdate中限制了更新') alert('shouldComponentUpdate中限制了更新') return false; } return true; }
效果:
建议去仓库看完整源码认真斟酌:
带diff算法版mini-React,选择diff分支
看加入了diff
算法后的效果
固然state
更新后,只是更新了对应的节点,所谓的diff
算法,就是将真实dom
和虚拟dom
对比后,直接dom
操做。操做那些有更新的节点~ 固然也有直接对比两个虚拟dom
对象,而后打补丁上去~咱们这种方式若是作SSR
同构就不行,由于咱们服务端没dom
对象这个说法,没法运行~
这段
diff
是有点硬核,可是去仓库认真看看,本身尝试写写,也是能够啃下来的。
state
版上面的版本,每次setState都会更新组件,这样很不友好,由于有可能一个操做会带来不少个setState,并且极可能会频繁更新state。为了优化性能,咱们把这些操做都放在一帧内去操做~
这里咱们使用requestAnimationFrame
,去执行合并操做~
首先更新setState
入口,不要直接从新渲染组件:
import { _render } from '../reactDom/index'; import { enqueueSetState } from './setState'; export class Component { constuctor(props = {}) { this.state = {}; this.props = props; } setState(stateChange) { // 将修改合并到state console.log('setstate'); const newState = Object.assign(this.state, stateChange); console.log('state:', newState); this.newState = newState; enqueueSetState(newState, this); } }
enqueueSetState
是咱们的一个入口函数:
function enqueueSetState(stateChange, component) { if (setStateQueue.length === 0) { //清空队列的办法是异步执行,下面都是同步执行的一些计算 defer(flush); } //向队列中添加对象 key:stateChange value:component setStateQueue.push({ stateChange, component }); //若是渲染队列中没有这个组件 那么添加进去 if (!renderQueue.some(item => item === component)) { renderQueue.push(component); } }
上面代码的精髓:
setState
调用进入if (setStateQueue.length === 0)
的判断flush
函数setStateQueue.push
renderQueue.push(component)
defer
函数defer
函数
function defer(fn) { //requestIdleCallback的兼容性很差,对于用户交互频繁屡次合并更新来讲 ,requestAnimation更有及时性高优先级,requestIdleCallback则适合处理能够延迟渲染的任务~ // if (window.requestIdleCallback) { // console.log('requestIdleCallback'); // return requestIdleCallback(fn); // } //高优先级任务 return requestAnimationFrame(fn); }
思考了好久,决定仍是用requestAnimationFrame
,为了体现界面交互的及时性
flush
清空队列的函数:
function flush() { let item, component; //依次取出对象,执行 while ((item = setStateQueue.shift())) { const { stateChange, component } = item; // 若是没有prevState,则将当前的state做为初始的prevState if (!component.prevState) { component.prevState = Object.assign({}, component.state); } // 若是stateChange是一个方法,也就是setState的第二种形式 if (typeof stateChange === 'function') { Object.assign( component.state, stateChange(component.prevState, component.props) ); } else { // 若是stateChange是一个对象,则直接合并到setState中 Object.assign(component.state, stateChange); } component.prevState = component.state; } //依次取出组件,执行更新逻辑,渲染 while ((component = renderQueue.shift())) { renderComponent(component); } }
flush
函数的精髓:
state
和组件的队列, 一个是须要更新的组件队列setState
队列的须要更新的组件,一次性合并清空完整代码仓库地址,欢迎star
:
带diff算法和异步state的minj-react
V15
版本的stack
递归diff
版本的React
实现:当咱们有100个节点须要更新的时候,咱们正在递归对比节点,此时用户点击界面须要弹框,那么可能会形成延迟弹出窗口,根据RAID
,超过100ms
,用户就会感受明显卡顿。为了防止出现这种状况,咱们须要改变总体的diff
策略。把递归的对比,改为能够暂停执行的循环对比,这样若是即时咱们在对比阶段,有用户点击须要交互的时候,咱们能够暂停对比,处理用户交互。
上面这段话,说的就是stack
版本和Fiber
架构的区别。
stack
版本就是咱们上面的版本
Fiber
版本:思路:
dom
对象的去diff
对比方式,单链表结构,三根指针,return children sibling
。requestAnimationFrame
,若是还有时间,那么就去执行requestIdleCallback
.这个版本暂时就结束了哦~ 欢迎加入咱们的前端交流群,还有前往gitHub
给个star
。
本人参考:
hujiulong的博客,感谢这些大佬的无私开源
前端交流群:
如今人数超过了100人,因此只能加我,而后拉大家进群!!
另外深圳招收跨平台开发
Electron+React
的即时通信产品前端工程师
欢迎投递: 453089136@qq.com
- Peter
招收中级和高级各一名~团队氛围nice
不加班