Skip to content

chinanf-boy/ink-explain

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 

Repository files navigation

ink

「 对CLI做出-React。 使用组件构建和测试您的CLI输出. 」

explain

Explanation

"version": "0.4.1"

github source

english


本次例子

-官方

Details
const {h, render, Component, Text} = require('ink');

class Counter extends Component {
	constructor() {
		super();

		this.state = {
			i: 0
		};
	}

	render() {
		return h('div', {}, [
			h('div', {}, []),
			h('div', {}, [
				h(Text, {blue: true}, '~/Projects/ink ')
			]),
			h('div', {}, [
				h(Text, {red: true}, 'λ '),
				h(Text, {green: true}, 'node '),
				h(Text, {}, 'media/example')
			]),
			h(Text, {green: true}, `${this.state.i} tests passed`)
		]);
	}

	componentDidMount() {
		console.log()
		this.timer = setInterval(() => {
			if (this.state.i === 50) {
				process.exit(0); // eslint-disable-line unicorn/no-process-exit
			}

			this.setState({
				i: this.state.i + 1
			});
		}, 100);
	}

	componentWillUnmount() {
		clearInterval(this.timer);
	}
}

render(h(Counter));


本目录


可以看到例子运行, render(h(Counter)); 首先触发 h


0. h

ink/lib/h.js

将-Component/"div/span/br"-变成-Vnode

Details
'use strict';

const flatten = require('lodash.flattendeep');
const VNode = require('./vnode');

// class 其实也是 函数的一种
module.exports = (component, props, ...children) => {
	if (typeof component !== 'function' && typeof component !== 'string') {
		throw new TypeError(`Expected component to be a function, but received ${typeof component}. You may have forgotten to export a component.`);
	}

	props = props || {};

	const readyChildren = [];

	// 孩子 
	if (children.length > 0) {
		props.children = children;
	}

	// 对那些 h(Component) / 其他类型 孩子改造
	flatten(props.children).forEach(child => {
		if (typeof child === 'number') {
			// 数字 孩子
			child = String(child);
		}

		if (typeof child === 'boolean' || child === null) {
			// 异类孩子
			child = '';
		}

		if (typeof child === 'string') {
			// 字符串孩子
			if (typeof readyChildren[readyChildren.length - 1] === 'string') {
				// 最后孩子是否同样是 字符串
				// 合并
				readyChildren[readyChildren.length - 1] += child;
			} else {
				readyChildren.push(child);
			}
		} else {
			// 剩下的就是 h(Component) -> new VNode 孩子
			readyChildren.push(child);
		}
		// 不管怎么样都放进 readyChildren
	});

	props.children = readyChildren;
// 把一系列的 选项打入 props 直接交给 VNode
	return new VNode(component, props);
};
  • VNode
  1. 有两种组件 div/span/br string 类型 | 通过 extend Component 类型
  1. VNode类可以说扩展-Component类, 也就是只是其中一个 VNode.component = component 属性
const getComponent = name => {
	switch (name) {
		case 'div': return Div; // 相当于 孩子 + 换行
		case 'span': return Span; // 相当于 孩子
		case 'br': return Br; // // 相当于 换行
		default: return null;
	}
};

class VNode {
	constructor(component, props = {}) {
		
		const ref = props.ref;
		delete props.ref;

		// 组件类
		this.component = typeof component === 'string' ? getComponent(component) : component;
		// 分析选项
		this._props = transformProps(props);
		this._children = [];
		// 指定的组件
		this.ref = ref;
		this.instance = null;
	}

	// 定义 - 属性规则
	get props() {
		return this._props;
	}

	set props(nextProps) {
		this._props = transformProps(nextProps);
		return this._props;
	}

	get children() {
		return this._children;
	}

	set children(nextChildren) {
		this._children = flatten(arrify(nextChildren));
		return this._children;
	}

	// 在一定时候, 需要将  Component 类 实例化
	createInstance(props) {
		// 只会将 继承 Component 的类实例化
		if (isClassComponent(this.component)) {
			this.instance = new this.component(props, {});
		}
	}
}

if (process.env.NODE_ENV !== 'production') {
	// 确定一个 不可变类型
	VNode.prototype.$$typeof = Symbol.for('react.element');
}

module.exports = VNode;

通过 hVNode 的 洗刷

例子🌰中的 Counter -> VNode

⚠️注意的是 Coumter.render 里面的 渲染, 并没有触发。

VNode(Counter) 只是一个很原型的 虚拟节点「VNode」, 没有孩子

👇下一步, 我们把 �统一好的VNode -> render(VNode)

但是在 render 中, 主要说得是, 接管终端 输出/ 输入


1. render

ink/index.js

代码 30-119

接管终端 输出/ 输入, 和重覆盖

Details
exports.render = (tree, options) => {
    // tree == h(Counter)
	if (options && typeof options.write === 'function') {
		options = {
			stdout: options
		};
	}

	const {stdin, stdout} = Object.assign({
		stdin: process.stdin, // cli-输入
		stdout: process.stdout // cli-输出
	}, options);

	const log = logUpdate.create(stdout); // log-update 是 对 通过覆盖终端中的前一个输出进行记录。

	const context = {};
	let isUnmounted = false;
	let currentTree;

    readline.emitKeypressEvents(stdin);
    // 相应于接收到的输入触发 'keypress' 事件。

	if (stdin.isTTY) {
        stdin.setRawMode(true);
//把 tty.ReadStream 配置成原始模式。
// 在原始模式中,输入按字符逐个生效,但不包括修饰符。
	}

	const update = () => {
		const nextTree = build(tree, currentTree, onUpdate, context); // 确定✅-下一个树
		log(renderToString(nextTree)); // 覆盖

		currentTree = nextTree;
	}; // 更新终端视图

	const onUpdate = () => { // 给予 diff 更新函数
		if (isUnmounted) {
			return;
		}

		update();
	};

	update(); // 先运行一边

	const onKeyPress = (ch, key) => {
		if (key.name === 'escape' || (key.ctrl && key.name === 'c')) {
			exit();// 退出
		}
	}; // 终端输入键-监控-触发事件

	if (stdin.isTTY) {
		stdin.on('keypress', onKeyPress); // 监控输入键
		stdout.on('resize', update); // 监控 终端重载
	}

	const consoleMethods = ['dir', 'log', 'info', 'warn', 'error'];

	consoleMethods.forEach(method => {
		const originalFn = console[method];

		console[method] = (...args) => {
			log.clear();
			log.done();
			originalFn.apply(console, args);
			update();
		};

		console[method].restore = () => {
			console[method] = originalFn;
		};
	}); // 控制 console.** 函数 对终端视图的输出

	const exit = () => {
		if (isUnmounted) { // 已经拆了
			return;
		}

		if (stdin.isTTY) {
			stdin.setRawMode(false); // 默认模式
			stdin.removeListener('keypress', onKeyPress); // 移除
			stdin.pause(); // 输入暂停
			stdout.removeListener('resize', update); // 移除
		}

		isUnmounted = true; // 拆了
        build(null, currentTree, onUpdate, context);
        // 终端视图-归零
		log.done(); // 最重要的是这个, 放开终端输出

		consoleMethods.forEach(method => console[method].restore()); // 把原来的 换回去
	};

	return exit;
};

相应于接收到的输入触发 'keypress' 事件。重置所有事件, 然后自定义事件

配置成原始模式。在原始模式中,输入按字符逐个生效,但不包括修饰符。

说白了, 所有的类React, 组件定义等等, 最后都输出到 log-update 这个控制终端输出的库中, 通过覆盖终端中的前一个输出进行记录。用于渲染进度条,动画等! log-update by sindresorhus

用来建立新的-视图树Tree, 也就❤️最重要❤️

把 视图树VNode 类 变成 String, 给 log-update 使用

log(renderToString(nextTree));


2. build

ink/lib/index.js

用来建立新的-视图树Tree

代码 19-23

const noop = () => {};

const build = (nextTree, prevTree, onUpdate = noop, context = {}) => {
    // 初始化 prevTree undefaulted
	return diff(prevTree, nextTree, onUpdate, context);
};

那么到了这里-diff-「有关 比较新旧树 触发生命周期钩子, 改变-虚拟树」

但在这例子中, 仅仅是自己与自己的状态比较, 有关setState 的触发机制



3. diff

ink/lib/diff.js

我们前面已经说明了, 在如何接管-终端输出和输入, 已达到覆盖的动画等效果

对比-视图树, 返回新或者没有变的树🌲

Details

⚠️ onUpdate 也跟进来了, 这是有关setState 触发重覆盖的问题

const diff = (prevNode, nextNode, onUpdate, context) => {
// 初始化时, prevNode 未声明
	if (typeof nextNode === 'number') { // 如果是 数字
		if (prevNode instanceof VNode) {
			unmount(prevNode);
		}

		return String(nextNode); // 转 字符串
	}

	if (!nextNode || typeof nextNode === 'boolean') { 
        // 如果是 正负
		if (prevNode instanceof VNode) {
			unmount(prevNode);
		}

		return null; // 返回 🈳️
	}

	if (typeof nextNode === 'string') { // 如果 字符串
		if (prevNode instanceof VNode) {
			unmount(prevNode);
		}

		return nextNode; // 返回 下个🌲
	}

    let isPrev = true; // 如果true , 不需要更新 
    // 其实改为 isPrev 可能好理解点, just do it

	if (!(prevNode instanceof VNode)) { // 如果上一个不是VNode
		mount(nextNode, context, onUpdate);
		isPrev = false; // 如果false, 更新-新的
	}

	if (isPrev && prevNode.component !== nextNode.component) { // 直接对比 Component 类
        unmount(prevNode); 
        // 移除旧, 触发 componentWillUnmount
        mount(nextNode, context, onUpdate); 
        // 加载新的 注意⚠️ onUpdate 是覆盖-终端输出的关键
        // 触发 componentWillMount
		isPrev = false;
    } 
    
// 如果到这里 isPrev 还是 == true, 说明自己和自己,判断还没有结束
//⏰ 下面这部分, 就开始细分更新, 和 剩下组件事件钩子的运行

    // 对比 Component props
    const shouldUpdate = isPrev && shouldComponentUpdate(prevNode, getProps(nextNode), getNextState(prevNode));

// 为什么是 prevNode, 因为还是 isPrev , 这里就自己和自己比较的开始了

    // 旧树的孩子
	const prevChildren = isPrev ? [].slice.call(prevNode.children) : [];

	if (isPrev && !isEqualShallow(getProps(prevNode), getProps(nextNode))) {
		componentWillReceiveProps(prevNode, getProps(nextNode));
	}

	if (shouldUpdate) { // 应该重新渲染
		rerender(prevNode, context); // ???
	}

    // 下一个树孩子
	const nextChildren = isPrev ? prevNode.children : nextNode.children;

	const length = Math.max(prevChildren.length, nextChildren.length);
	const reconciledChildren = [];

	for (let index = 0; index < length; index++) { 
        // 递归把 孩子 整回来
        const childNode = diff(prevChildren[index], nextChildren[index], onUpdate, context);
        
		reconciledChildren.push(childNode);
	}

	if (isPrev) {
        // 旧树
		prevNode.children = reconciledChildren;

		if (shouldUpdate) {
            // 触发更新-旧树
			componentDidUpdate(prevNode);
		}
	} else {
        // 新树
        nextNode.children = reconciledChildren;
         // 触发更新
		componentDidMount(nextNode);
    }
    // 反正 componentDidMount 是一定要触发的

	return isPrev ? prevNode : nextNode;
};

3.1 mount

  1. 将 vnode.component 实例化 vnode.instance
  1. 触发 载入前钩子-componentWillMount
  1. _render()触发->生成孩子虚拟树
const mount = (vnode, context, onUpdate) => {
	const props = getProps(·);
	checkPropTypes(vnode.component, props);

	if (isClassComponent(vnode.component)) {
// 1. 继承Component的类
		vnode.createInstance(props);
		vnode.instance._onUpdate = onUpdate;

		// context 是 一个全局上下文
		vnode.instance.context = Object.assign(context, vnode.instance.getChildContext());
		// 2.
		vnode.instance.componentWillMount();
		// 3.
		vnode.children = vnode.instance._render();
	} else {
// 关于 孩子 换行之类
// div/span/br
		vnode.children = vnode.component(props, context);
	}
};

3.2 unmount

  1. 触发 卸载前-componentWillUnmount
  1. 清空 实例
  1. 全部孩子去掉
  1. 去掉指定组件
const unmount = vnode => {
	if (isClassComponent(vnode.component)) {
		componentWillUnmount(vnode);
		vnode.instance = null;
	}

	vnode.children.forEach(childVNode => {
		diff(childVNode, null);
	});

// 去掉全局变量
	if (isClassComponent(vnode.component) && vnode.ref) {
		vnode.ref(null);
	}
};

🧠 在diff函数我们需要什么? return isPrev ? prevNode : nextNode;

那么 我们需要一个改造好的 虚拟树。

这么一堆的判断, 生命周期钩子触发, Instance化, 但我们返回的也就是-VNode类,

只是 VNode.instance 不再是 null,

VNode.children_render()触发 回来 变成 VNode


4. renderToString

Vnode树 变成 String

Details

/ink/lib/render-to-string.js

'use strict';

const StringComponent = require('./string-component');

const renderToString = vnode => {
	if (!vnode) {
// 1.
		return '';
	}

	if (typeof vnode === 'string') {
// 2.
		return vnode; // children 返回
	}

	if (Array.isArray(vnode)) { // 数组VNode, h(Component, props, children)
	// children 都是数组, 所有通过这样 join('') 组合
		return vnode
			.map(renderToString)
			.join('');
	}

	if (vnode.instance instanceof StringComponent) {

// 真正停止的递归终止条件, 
// 1. ''
// 2. String
// 3. 是 继承 StringComponent Text

// 也就是 Text, 作者定义的字符串组件
// 可以看到本项目例子中 Text, 

		// 我们上面 3.1 mount 就说了 vnode.children 是 vnode.instance._render() 赋予的
		// 4.1 StringComponent children 如何是字符串
		const children = renderToString(vnode.children);
		// 4.2  染色字符串
		return vnode.instance.renderString(children); 
	}

	// 递归-孩子
	return renderToString(vnode.children);
};

module.exports = renderToString;

4.1 StringComponent children 如何是字符串

ink/lib/string-component.js

class StringComponent extends Component {
	render() {
		return this.props.children;
	}
}

可以看到本项目例子中 Text

h(Text, {blue: true}, '~/Projects/ink ') =>

'~/Projects/ink ' == props.children =>

vnode.instance._render() 触发 StringComponent.render() =>

vnode.children = this.props.children == '~/Projects/ink '


4.2 染色字符串

ink/lib/components/text.js

代码 22-36

// 没错, 为什么可以设置颜色, 就是 chalk 输出颜色库
// h(Text, {blue: true}, '~/Projects/ink ')

class Text extends StringComponent {
	renderString(children) {
		Object.keys(this.props).forEach(method => {
			if (this.props[method]) {
				if (methods.includes(method)) {
					children = chalk[method].apply(chalk, arrify(this.props[method]))(children);
				} else if (typeof chalk[method] === 'function') {
					children = chalk[method](children);
				}
			}
		});

		return children; // 染好的字符串
	}
}

5. setState

Component 组件中改变 状态的函数, 却也是触发, 重覆盖的函数

怎么做到, 点击⬇️

Details

让我们回到 Component 类的定义

ink/lib/component.js

代码 15-27

	setState(nextState, callback) {
		if (typeof nextState === 'function') {
			nextState = nextState(this.state, this.props);
		}

		this._pendingState = Object.assign({}, this._pendingState || this.state, nextState);

		if (typeof callback === 'function') {
			this._stateUpdateCallbacks.push(callback);
		}

		this._enqueueUpdate(); // 《=== 触发
	}

代码 70-72

	_enqueueUpdate() {
		enqueueUpdate(this._onUpdate); 
		// 把 组件的 _onUpdate 放进去

//🤔 _onUpdate 怎么来的, 其实本质就是
// `ink/index.js` 61-67

	// const onUpdate = () => {
	// 	if (isUnmounted) {
	// 		return;
	// 	}

	// 	update();
	// };

// 在做比较-diff-函数时就一直在变量中流传
// diff(prevTree, nextTree, onUpdate, context);
	}

代码 3

const {enqueueUpdate} = require('./render-queue');

ink/lib/render-queue.js

'use strict';

const options = require('./options');

const queue = [];

const rerender = () => {
	while (queue.length > 0) {
		const callback = queue.pop();
		callback();
	}
};

exports.rerender = rerender;

exports.enqueueUpdate = callback => {
	queue.push(callback);
	options.deferRendering(rerender); 
	// <=== 函数在下一次事件轮询调用
	// 也就是 onUpdate 的调用
};

ink/lib/options.js

'use strict';

module.exports = {
	deferRendering: process.nextTick //<===
};

一旦当前事件轮询队列的任务全部完成,在next tick队列中的所有callbacks会被依次调用。

About

explain : 「 ink 」对CLI做出-React。 使用组件构建和测试您的CLI输出

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published