想要本身實現一個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
不加班