現在網上有很多react原理解析這樣的文章,但是往往這樣的文章我看完過後卻沒有什麼收穫,因為行文思路太快,大部分就是寫了幾句話簡單介紹下這段程式碼是用來幹嘛的,然後就貼上原始碼讓你自己看,有可能作者本人是真的看懂了,但是對於大部分閱讀這篇文章的人來說,確是雲裡霧裡。
講解一個框架的原始碼,最好的方式就是實現一個簡易版的,這樣在你實現的過程中,讀者就能瞭解到你整體的思路,也就能站在更高的層面上對框架有一個整體的認知,而不是陷在一些具體的技術細節上。
這篇文章就非常棒的實現了一個簡單的react框架,接下來屬於對原文的翻譯加上一些自己在使用過程中的理解。
首先先整體介紹通過這篇文章你能學到什麼–我們將實現一個簡單的React,包括簡單的元件級api和虛擬dom,文章也將分為以下四個部分
- Elements:在這一章我們將學習JSX是如何被處理成虛擬DOM的
- Rendering: 在這一小節我們將想你展示如何將虛擬dom變成真實的DOM的
- Patching: 在這一章我們將向你展示為什麼key如此重要,並且如何利用虛擬DOM對已存在的DOM進行批量更新
- Components :最後一小節將告訴你React元件和他的生命週期
Element
元素攜帶者很多重要的資訊,比如節點的type,props,children list,根據這些屬性,能渲染出我們需要的元素,它的樹形結構如下
{
"type": "ul", "props": {
"className": "some-list"
}, "children": [ {
"type": "li", "props": {
"className": "some-list__item"
}, "children": [ "One" ]
}, {
"type": "li", "props": {
"className": "some-list__item"
}, "children": [ "Two" ]
} ]
}複製程式碼
但是如果我們日常寫程式碼如果要寫成這個樣子,那我們應該要瘋了,所以一般我們會寫jsx的語法
/** @jsx createElement */const list = <
ul className="some-list">
<
li className="some-list__item">
One<
/li>
<
li className="some-list__item">
Two<
/li>
<
/ul>
;
複製程式碼
為了能夠讓他被編譯成常規的方法,我們需要加上註釋來定義用哪個函式,最終定義的函式被執行,最後會返回給一個虛擬DOM
const createElement = (type, props, ...children) =>
{
props = props != null ? props : {
};
return {type, props, children
};
};
複製程式碼
我為什麼這個地方要加註釋呢,因為我在用babel打包jsx的語法的時候,貌似預設用的React裡提供的CreateElement,所以當時我配置了.babelrc以後
發現它報了一個React is not defined錯誤,但是我安裝的是作者這個簡易的類React包,後來才知道在jsx前要加一段註釋來告訴babel編譯的時候用哪個函式
/** @jsx Gooact.createElement */複製程式碼
Rendering
這一節是將vdom渲染真實dom
上一節我們已經得到了根據jsx語法得出的虛擬dom樹形結構,那麼就該將這個虛擬dom結構渲染成真實dom
那麼我們在拿到一個樹形結構的時候,如何判斷這個節點應該渲染成真實dom的什麼樣子呢,這裡就會有3種情況,第一種就是直接會返回一個字串,那我們就直接生成一個文字節點,如果返回的是一個我們自定義的元件,那麼我們就在呼叫這個方法,如果是一個常規的dom元件,我們就建立這樣的一個dom元素,然後接著繼續遍歷它的子節點。
setAttribute就是將我們設定在虛擬dom上的屬性設定在真實dom上
const render = (vdom, parent=null) =>
{
if (parent) parent.textContent = '';
const mount = parent ? (el =>
parent.appendChild(el)) : (el =>
el);
if (typeof vdom == 'string' || typeof vdom == 'number') {
return mount(document.createTextNode(vdom));
} else if (typeof vdom == 'boolean' || vdom === null) {
return mount(document.createTextNode(''));
} else if (typeof vdom == 'object' &
&
typeof vdom.type == 'function') {
return mount(Component.render(vdom));
} else if (typeof vdom == 'object' &
&
typeof vdom.type == 'string') {
const dom = document.createElement(vdom.type);
for (const child of [].concat(...vdom.children)) // flatten dom.appendChild(render(child));
for (const prop in vdom.props) setAttribute(dom, prop, vdom.props[prop]);
return mount(dom);
} else {
throw new Error(`Invalid VDOM: ${vdom
}.`);
}
};
const setAttribute = (dom, key, value) =>
{
if (typeof value == 'function' &
&
key.startsWith('on')) {
const eventType = key.slice(2).toLowerCase();
dom.__gooactHandlers = dom.__gooactHandlers || {
};
dom.removeEventListener(eventType, dom.__gooactHandlers[eventType]);
dom.__gooactHandlers[eventType] = value;
dom.addEventListener(eventType, dom.__gooactHandlers[eventType]);
} else if (key == 'checked' || key == 'value' || key == 'id') {
dom[key] = value;
} else if (key == 'key') {
dom.__gooactKey = value;
} else if (typeof value != 'object' &
&
typeof value != 'function') {
dom.setAttribute(key, value);
}
};
複製程式碼
Patching
想象一個你有一個很深的結構,而且你還需要頻繁的更新你的虛擬dom,如果你改變了一些,那麼全部都要渲染,這無疑會消耗很多時間。
但是如果我們有一個演算法能夠比較出新的虛擬dom和已有dom的差異,然後只更新那些改變的地方,這個地方就是經常說的React團隊做了一些經過實踐後的約定,將本來o(n)^3的時間複雜度降低到了o(n),主要就是下面兩種主要的約定
- 兩個元素如果有不同的型別那麼就會產生兩種不同的樹
- 當我們給了一個key屬性後,他就會根據它去判斷
const patch = (dom, vdom, parent=dom.parentNode) =>
{
const replace = parent ? el =>
(parent.replaceChild(el, dom) &
&
el) : (el =>
el);
if (typeof vdom == 'object' &
&
typeof vdom.type == 'function') {
return Component.patch(dom, vdom, parent);
} else if (typeof vdom != 'object' &
&
dom instanceof Text) {
return dom.textContent != vdom ? replace(render(vdom)) : dom;
} else if (typeof vdom == 'object' &
&
dom instanceof Text) {
return replace(render(vdom));
} else if (typeof vdom == 'object' &
&
dom.nodeName != vdom.type.toUpperCase()) {
return replace(render(vdom));
} else if (typeof vdom == 'object' &
&
dom.nodeName == vdom.type.toUpperCase()) {
const pool = {
};
const active = document.activeElement;
for (const index in Array.from(dom.childNodes)) {
const child = dom.childNodes[index];
const key = child.__gooactKey || index;
pool[key] = child;
} const vchildren = [].concat(...vdom.children);
// flatten for (const index in vchildren) {
const child = vchildren[index];
const key = child.props &
&
child.props.key || index;
dom.appendChild(pool[key] ? patch(pool[key], child) : render(child));
delete pool[key];
} for (const key in pool) {
if (pool[key].__gooactInstance) pool[key].__gooactInstance.componentWillUnmount();
pool[key].remove();
} for (const attr of dom.attributes) dom.removeAttribute(attr.name);
for (const prop in vdom.props) setAttribute(dom, prop, vdom.props[prop]);
active.focus();
return dom;
}
};
複製程式碼
Component
元件是最像js中函式的概念了,我們通過它能夠展示出什麼應該展示在螢幕上,它可以被定義成一個無狀態的函式,或者是一個有生命週期的元件。複製程式碼
class Component {
constructor(props) {
this.props = props || {
};
this.state = null;
} static render(vdom, parent=null) {
const props = Object.assign({
}, vdom.props, {children: vdom.children
});
if (Component.isPrototypeOf(vdom.type)) {
const instance = new (vdom.type)(props);
instance.componentWillMount();
instance.base = render(instance.render(), parent);
instance.base.__gooactInstance = instance;
instance.base.__gooactKey = vdom.props.key;
instance.componentDidMount();
return instance.base;
} else {
return render(vdom.type(props), parent);
}
} static patch(dom, vdom, parent=dom.parentNode) {
const props = Object.assign({
}, vdom.props, {children: vdom.children
});
if (dom.__gooactInstance &
&
dom.__gooactInstance.constructor == vdom.type) {
dom.__gooactInstance.componentWillReceiveProps(props);
dom.__gooactInstance.props = props;
return patch(dom, dom.__gooactInstance.render());
} else if (Component.isPrototypeOf(vdom.type)) {
const ndom = Component.render(vdom);
return parent ? (parent.replaceChild(ndom, dom) &
&
ndom) : (ndom);
} else if (!Component.isPrototypeOf(vdom.type)) {
return patch(dom, vdom.type(props));
}
} setState(nextState) {
if (this.base &
&
this.shouldComponentUpdate(this.props, nextState)) {
const prevState = this.state;
this.componentWillUpdate(this.props, nextState);
this.state = nextState;
patch(this.base, this.render());
this.componentDidUpdate(this.props, prevState);
} else {
this.state = nextState;
}
} shouldComponentUpdate(nextProps, nextState) {
return nextProps != this.props || nextState != this.state;
} componentWillReceiveProps(nextProps) {
return undefined;
} componentWillUpdate(nextProps, nextState) {
return undefined;
} componentDidUpdate(prevProps, prevState) {
return undefined;
} componentWillMount() {
return undefined;
} componentDidMount() {
return undefined;
} componentWillUnmount() {
return undefined;
}
}複製程式碼
本次文章中新開發的gooact輪子就結束了,讓我們看看他有什麼功能
- 它能夠高效的更新複雜的dom結構
- 支援函式式和狀態式兩種元件
那它距離一個完整的React應用還差什麼呢?
- 他還不支援fragments,portals這樣的新版本的特性
- 因為React Fiber太複雜了,目前還沒有支援
- 如果你寫了重複的key,可能會有bug
- 對於一些方法,還少了一些回撥函式
但是這篇文章是不是給你帶來一個全新的視角看React框架,讓你對這個框架做的事情有了一個全域性的瞭解呢?
反正筆者看了原文對React框架思路又更加清晰了,最後獻上使用這個框架的用例demo
來源:https://juejin.im/post/5b0a697f518825389c508872?utm_medium=fe&utm_source=weixinqun