精讀《用160行js程式碼實現一個React》

2014_發表於2018-05-27

現在網上有很多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

相關文章