babel
在runtime時, babel轉譯器將對每一個節點都執行在編譯註釋(Pragma)中宣告的函式。 例如:
before babel: (the code you write)
/** @jsx h */
let foo = <div id="foo">Hello!</div>;
複製程式碼
after babel: (the code you run)
var foo = h('div', {id:"foo"}, 'Hello!');
複製程式碼
也可以在瀏覽器裡列印看一下結果:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<script type="text/jsx" data-presets="es2016,react">
/** @jsx h */
let foo = <div id="foo">Hello!</div>;
const h = (type, attributes, ...args) => {
let children = args.length ? [].concat(...args) : null;
return { nodeName, attributes, children };
}
console.log(foo);
// foo: {
// attributes: {id: "foo"}
// children: ["Hello!"]
// nodeName: "div"
// }
</script>
</body>
</html>
複製程式碼
createElement
將jsx中的element轉換成 plain object
function createElement(type, config, ...args) {
const props = Object.assign({}, config);
const hasChildren = args.length > 0;
const rawChildren = hasChildren ? [].concat(...args) : [];
props.children = rawChildren
.filter(c => c != null && c !== false)
.map(c => c instanceof Object ? c : createTextElement(c));
return { type, props };
}
function createTextElement(value) {
return createElement(TEXT_ELEMENT, { nodeValue: value });
}
複製程式碼
這裡對text node特殊處理了一下,使其不那麼特殊, 如此後面處理時省略了許多判斷
instance
每個element(jsx轉換而來的)對應一個instance,instance包含了dom, element, childInstances。 如果是custom component 則還有一個publicInstance,是元件的例項.
function instantiate(element) {
const { type, props } = element;
const isDomElement = typeof type === "string";
// 這裡對custom component與built-in component分別做處理
if (isDomElement) {
const isTextElement = type === TEXT_ELEMENT;
const dom = isTextElement
? document.createTextNode("")
: document.createElement(type);
// updateDomProperties 作用是更新dom上的屬性,比如class、id、或者刪除新增事件監聽函式等
updateDomProperties(dom, [], props);
// 處理子元件
const childElements = props.children || [];
const childInstances = childElements.map(instantiate);
const childDoms = childInstances.map(childInstance => childInstance.dom);
// 將子元件的例項插入到dom上
childDoms.forEach(childDom => dom.appendChild(childDom));
const instance = { dom, element, childInstances };
return instance;
} else {
const instance = {};
// 或者custom-component 例項
const publicInstance = createPublicInstance(element, instance);
// 呼叫render方法,獲得子元件,再獲取child instance.
// 注意這裡是childInstance而不是childInstances,因為custom component只能一個子元件,不能是陣列
const childElement = publicInstance.render();
const childInstance = instantiate(childElement);
const dom = childInstance.dom;
Object.assign(instance, { dom, element, childInstance, publicInstance });
return instance;
}
}
複製程式碼
createPublicInstance
function createPublicInstance(element, internalInstance) {
const { type, props } = element;
const publicInstance = new type(props);
publicInstance.__internalInstance = internalInstance;
return publicInstance;
}
複製程式碼
這裡的internalInstance其實就是 instance,之所以需要將其繫結到元件例項上是因為 在自定義元件中update需要用到,這裡可以先看一下Component的setState實現
setState(partialState) {
this.state = Object.assign({}, this.state, partialState);
updateInstance(this.__internalInstance);
}
function updateInstance(internalInstance) {
const parentDom = internalInstance.dom.parentNode;
const element = internalInstance.element;
reconcile(parentDom, internalInstance, element);
}
複製程式碼
reconciliation 對元件進行 diff
下面instance是當前的狀態,而element是新的狀態,將這兩者進行diff,就可以得出如何更新dom節點
function reconcile(parentDom, instance, element) {
if (instance == null) {
// 不存在instance則建立一個
const newInstance = instantiate(element);
parentDom.appendChild(newInstance.dom);
return newInstance;
} else if (element == null) {
// 不存在element說明該element已經被刪除
parentDom.removeChild(instance.dom);
return null;
} else if (instance.element.type !== element.type) {
// 如果新舊element型別不同則替換
const newInstance = instantiate(element);
parentDom.replaceChild(newInstance.dom, instance.dom);
return newInstance;
} else if (typeof element.type === "string") {
// build-in component 更新屬性
updateDomProperties(instance.dom, instance.element.props, element.props);
instance.childInstances = reconcileChildren(instance, element);
instance.element = element;
return instance;
} else {
// 更新custom component的屬性
instance.publicInstance.props = element.props;
const childElement = instance.publicInstance.render();
const oldChildInstance = instance.childInstance;
const childInstance = reconcile(
parentDom,
oldChildInstance,
childElement
);
instance.dom = childInstance.dom;
instance.childInstance = childInstance;
instance.element = element;
return instance;
}
}
function reconcileChildren(instance, element) {
const dom = instance.dom;
const childInstances = instance.childInstances;
const nextChildElements = element.props.children || [];
const newChildInstances = [];
const count = Math.max(childInstances.length, nextChildElements.length);
for (let i = 0; i < count; i++) {
const childInstance = childInstances[i];
const childElement = nextChildElements[i];
const newChildInstance = reconcile(dom, childInstance, childElement);
newChildInstances.push(newChildInstance);
}
// 過濾掉已經被刪除的element
return newChildInstances.filter(instance => instance != null);
}
複製程式碼
Component
由於之前已經講過了setState,其他部分就很簡單了
class Component {
constructor(props) {
this.props = props;
this.state = this.state || {};
}
setState(partialState) {
this.state = Object.assign({}, this.state, partialState);
updateInstance(this.__internalInstance);
}
}
複製程式碼
render
function render(element, container) {
reconcile(container, null, element);
}
複製程式碼
一個完整的例子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="root"></div>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<script type="text/jsx" data-presets="es2016,react">
/** @jsx Didact.createElement */
const Didact = importFromBelow();
class App extends Didact.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
add = () => {
this.setState({
count: this.state.count + 1,
})
}
render() {
return (
<div>
<h1>count: {this.state.count}</h1>
<Button onClick={this.add} />
</div>
);
}
}
class Button extends Didact.Component {
render() {
const { onClick } = this.props;
return (
<button onClick={onClick}>add</button>
);
}
}
Didact.render(<App />, document.getElementById("root"));
function importFromBelow() {
let rootInstance = null;
const TEXT_ELEMENT = "TEXT_ELEMENT";
function createElement(type, config, ...args) {
const props = Object.assign({}, config);
const hasChildren = args.length > 0;
const rawChildren = hasChildren ? [].concat(...args) : [];
props.children = rawChildren
.filter(c => c != null && c !== false)
.map(c => c instanceof Object ? c : createTextElement(c));
return { type, props };
}
function createTextElement(value) {
return createElement(TEXT_ELEMENT, { nodeValue: value });
}
function render(element, container) {
const prevInstance = rootInstance;
const nextInstance = reconcile(container, prevInstance, element);
rootInstance = nextInstance;
}
function reconcile(parentDom, instance, element) {
if (instance == null) {
const newInstance = instantiate(element);
parentDom.appendChild(newInstance.dom);
return newInstance;
} else if (element == null) {
parentDom.removeChild(instance.dom);
return null;
} else if (instance.element.type !== element.type) {
const newInstance = instantiate(element);
parentDom.replaceChild(newInstance.dom, instance.dom);
return newInstance;
} else if (typeof element.type === "string") {
updateDomProperties(instance.dom, instance.element.props, element.props);
instance.childInstances = reconcileChildren(instance, element);
instance.element = element;
return instance;
} else {
instance.publicInstance.props = element.props;
const childElement = instance.publicInstance.render();
const oldChildInstance = instance.childInstance;
const childInstance = reconcile(
parentDom,
oldChildInstance,
childElement
);
instance.dom = childInstance.dom;
instance.childInstance = childInstance;
instance.element = element;
return instance;
}
}
function reconcileChildren(instance, element) {
const dom = instance.dom;
const childInstances = instance.childInstances;
const nextChildElements = element.props.children || [];
const newChildInstances = [];
const count = Math.max(childInstances.length, nextChildElements.length);
for (let i = 0; i < count; i++) {
const childInstance = childInstances[i];
const childElement = nextChildElements[i];
const newChildInstance = reconcile(dom, childInstance, childElement);
newChildInstances.push(newChildInstance);
}
return newChildInstances.filter(instance => instance != null);
}
function instantiate(element) {
const { type, props } = element;
const isDomElement = typeof type === "string";
if (isDomElement) {
const isTextElement = type === TEXT_ELEMENT;
const dom = isTextElement
? document.createTextNode("")
: document.createElement(type);
updateDomProperties(dom, [], props);
const childElements = props.children || [];
const childInstances = childElements.map(instantiate);
const childDoms = childInstances.map(childInstance => childInstance.dom);
childDoms.forEach(childDom => dom.appendChild(childDom));
const instance = { dom, element, childInstances };
return instance;
} else {
const instance = {};
const publicInstance = createPublicInstance(element, instance);
const childElement = publicInstance.render();
const childInstance = instantiate(childElement);
const dom = childInstance.dom;
Object.assign(instance, { dom, element, childInstance, publicInstance });
return instance;
}
}
function updateDomProperties(dom, prevProps, nextProps) {
const isEvent = name => name.startsWith("on");
const isAttribute = name => !isEvent(name) && name != "children";
Object.keys(prevProps).filter(isEvent).forEach(name => {
const eventType = name.toLowerCase().substring(2);
dom.removeEventListener(eventType, prevProps[name]);
});
Object.keys(prevProps).filter(isAttribute).forEach(name => {
dom[name] = null;
});
Object.keys(nextProps).filter(isAttribute).forEach(name => {
dom[name] = nextProps[name];
});
Object.keys(nextProps).filter(isEvent).forEach(name => {
const eventType = name.toLowerCase().substring(2);
dom.addEventListener(eventType, nextProps[name]);
});
}
function createPublicInstance(element, internalInstance) {
const { type, props } = element;
const publicInstance = new type(props);
publicInstance.__internalInstance = internalInstance;
return publicInstance;
}
class Component {
constructor(props) {
this.props = props;
this.state = this.state || {};
}
setState(partialState) {
this.state = Object.assign({}, this.state, partialState);
updateInstance(this.__internalInstance);
}
}
function updateInstance(internalInstance) {
const parentDom = internalInstance.dom.parentNode;
const element = internalInstance.element;
reconcile(parentDom, internalInstance, element);
}
return {
createElement,
render,
Component
};
}
</script>
</body>
</html>
複製程式碼
總結
本文是對文章Didact: a DIY guide to build your own React 的總結。旨在幫助在閱讀react原始碼之前先整體把握react的結構組成,幫助更好的閱讀原始碼。由於react16大量更新了程式碼,引入了Fiber:Incremental reconciliation,下篇看一下fiber的簡單的實現。是同一位作者所寫。