profile
viewpoint
忽如寄 huruji 撩我 Shenzhen http://tink.greywind.xyz 忽如寄也叫灰风GreyWind。 want to be a hero one day

startedtranbathanhtung/react-fiber-implement

started time in 9 hours

startedarkingc/note

started time in a day

startedgatieme/CodingInterviews

started time in 2 days

starteddai-shi/reactive-react-redux

started time in 2 days

startedanswershuto/learnVue

started time in 3 days

issue commenthuruji/JDS_weekly

2019第四周

形象地解释了 css 选择器优先级地问题

image

huruji

comment created time in 3 days

startedvlang/v

started time in 3 days

PublicEvent

startedwojtekmaj/react-lifecycle-methods-diagram

started time in 5 days

startedykfe/egg-react-ssr

started time in 5 days

issue openedhuruji/blog

动手实现简单版的 React(二)— list diff

eJ2h8HGfo_M的副本

在上篇文章中,已经基本可以完成视图的更新了,其实就目前来说,对于一个数组list,如果只是对数组进行 pushpop 来说,目前已经完成的很好了。 每次更新的时候可以打开开发者工具选择 Element 面板,看到每次操作影响的DOM,影响的DOM会存在闪动的效果。但是考虑一下数组移动的情况:

如将 [1,2,3,4] 修改为 [2,1,4,3],如果你的这些 List 结构一致,可能仅仅是更新文本内容,但是如果内部可能会因数据的不同渲染不同的组件,那么这个时候甚至需要重新 tear down 所有的旧节点,然后再挂载新节点,也就是,create 2,delete1,create1,delete2,依次类推。同理如果是在中间插入也就会出现这种影响。看到这里,相信不会再有虚拟DOM比DOM操作快的这种谬误了,本质上来说,如果真的操作dom,肯定会是移动1或者2,再移动3或者4的,这才是最快的方法。

那么现在的问题就在于如何记录对应操作(增、删、移)?对于增删,相对来说比价简单,

React 聪明地通过记录一个 LastIndex 的索引,目的就是只有大于 index 值的元素才标记为移动,也就是说,react 的移动是前面的元素向后移动的,这样就保证能够正确的移动了。

那么大致的过程就是遍历新节点,找出对应的增加和移动操作,遍历旧节点,找到删除操作,这样就得到的补丁(patch),然后根据得到的补丁更新 Dom。

照例先编写类型:

export interface keyChanges {
  type: 'insert' | 'remove' | 'move',
  item: VdomInterface,
  afterNode?: VdomInterface,
  index: number
}

根据只有 key 属性的 list 才做这样的 diff,判断是否含有 key:

function isKeyChildren(oldChildren: Vdom, newChildren: Vdom):boolean {
  return !!(oldChildren && oldChildren[0] && oldChildren[0].props && oldChildren[0].props.key && newChildren && newChildren[0] && newChildren[0].props && newChildren[0].props.key)
}

在 diff 过程中,判断是否含有 key,含有的话就根据上面的原则进行 diff:

      const _component = dom._component as VdomInterface

      const isKeyed = isKeyChildren(_component.children, vdom.children)
      if(!isKeyed) {
        for(let i = 0; i < max; i++) {
          diff(dom.childNodes[i] || null, vdom.children[i] || null, dom)
        }
      } else {
        const patchesList = diffKeyChildren(_component.children, vdom.children)
        patch(patchesList, dom)
      }

这时候整个 diff 的代码如下:

export default function diff(dom: Dom, vdom, parent: Dom = dom.parentNode):void {
  if(!dom) {
    render(vdom, parent)
  } else if (!vdom) {
    dom.parentNode.removeChild(dom)
  } else if ((typeof vdom === 'string' || typeof vdom === 'number') && dom.nodeType === 3) {
    if(vdom !== dom.textContent) dom.textContent = vdom + ''
	} else if (vdom.nodeType === 'classComponent' || vdom.nodeType === 'functionalComponent') {
		const _component = dom._component as ClassComponent
		if (_component.constructor === vdom.type) {
      _component.props = vdom.props
      diff(dom, _component.render())
		} else {
      const newDom = render(vdom, dom.parentNode)
      dom.parentNode.replaceChild(newDom, dom)
    }
	} else if (vdom.nodeType === 'node') {
    if(!isSameNodeType(dom, vdom)) {
      const newDom = render(vdom, parent)
      dom.parentNode.replaceChild(newDom, dom)
    } else {
      const max = Math.max(dom.childNodes.length, vdom.children.length)
      diffAttribute(dom as HTMLElement, dom._component.props, vdom.props)
      const _component = dom._component as VdomInterface

      const isKeyed = isKeyChildren(_component.children, vdom.children)
      if(!isKeyed) {
        for(let i = 0; i < max; i++) {
          diff(dom.childNodes[i] || null, vdom.children[i] || null, dom)
        }
      } else {
        const patchesList = diffKeyChildren(_component.children, vdom.children)
        patch(patchesList, dom)
      }
    }
	}
}

根据上面 list diff 的分析,对应的代码如下:

export default function diffChildren(oldVdom: VdomInterface[], newVdom: VdomInterface[]):keyChanges[] {
	const changes = []
	let lastIndex = 0
	let lastPlacedNode = null
	const oldVdomKey = oldVdom.map(v => v.props.key)
	const newVdomKey = newVdom.map(v => v.props.key)

	newVdom.forEach((item, i) => {
		const index = oldVdomKey.indexOf(item.props.key)
		if (index === -1) {
			changes.push({
				type: 'insert',
				item: item,
				afterNode: lastPlacedNode
			})
			lastPlacedNode = item
		} else {
			if (index < lastIndex) {
				changes.push({
					type: 'move',
					item: oldVdom[index],
					afterNode: lastPlacedNode
				})
			}
			lastIndex = Math.max(index, lastIndex)
			lastPlacedNode = oldVdom[index]
		}
	})

	oldVdom.forEach((item, i) => {
		if (newVdomKey.indexOf(item.props.key) === -1) {
			changes.push({
				type: 'remove',
				index: i,
				item
			})
		}
	})

	return changes
}

方便测试,可以将其简化为对应的数组diff,如下:

function diffChildren(oldVdom, newVdom) {
  const changes = []
  let lastIndex = 0
  let lastPlacedNode = null

  newVdom.forEach((item, i) => {
    const index = oldVdom.indexOf(item);
    if (index === -1) {
      changes.push({
        type: 'insert',
        item: item,
        afterNode: lastPlacedNode
      })
    } else {
      if (index < lastIndex) {
        changes.push({
          type: 'move',
          item: item,
          afterNode: lastPlacedNode
        })
      }
      lastIndex = Math.max(index, lastIndex)
    }

    lastPlacedNode = item

  })

  oldVdom.forEach((item, i) => {
    if (newVdom.indexOf(item) === -1) {
      changes.push({
        type: 'remove',
        index: i
      })
    }
  })

  return changes
}

编写测试:

const changes = diffChildren([1, 2, 3, 7, 4], [1, 4, 5, 3, 7, 6])
console.log(changes)

运行可以看到对应的更改的结构输出:

拿到对应的操作记录补丁,就需要应用到真实的DOM中,如下:

import render from './render'
import { keyChanges } from './types/keyChanges'
import { Dom } from './types/dom'
import { VdomInterface, Vdom } from './types/vdom'

export default function patch(changes: keyChanges[], dom: Dom):void {
	changes.forEach(change => {
		switch (change.type) {
			case 'insert': {
				const node = change.afterNode.base
				const parent = node.parentNode as Node
				const child = render(change.item, parent)
				parent.insertBefore(child, node.nextSibling)
			}
				break;
			case 'remove':{
				const removedNode = change.item.base as Node
				removedNode.parentNode.removeChild(removedNode)
			}
				break;
			case 'move': {
				const node = change.item.base
				const afterNode = change.afterNode.base as Node
				node.parentNode.insertBefore(node,afterNode.nextSibling)
			}
			break;
			default:
		}
	})

	const vchildren = Array.from(dom.childNodes).map((e: Dom):Vdom => e._component as VdomInterface)
	const _component: VdomInterface = dom._component as VdomInterface
	_component.children = vchildren
}

这里需要注意的是我们需要更新父节点的 _component.children 属性的值,让他始终保持最新,这样保证下一次 list diff 是正确的。

编写对应的测试代码:

export default class App extends Component<any, any> {
	public state = {
		list: [1, 2, 3, 7, 4]
	}
	constructor(props) {
		super(props)
	}

	update() {
		const { list } = this.state
		this.setState({
			list: list.indexOf(4) > 2 ? [1, 4, 5, 3, 7, 6] : [1, 2, 3, 7, 4]
		})
	}

	render() {
		const { list } = this.state
		return (
			<div>
				<h1> list diff</h1>
				<div className="optcontainer">
					<div className="opt" onClick={this.update.bind(this)}>
						update
					</div>
				</div>
				<ul>{list.map(l => <li key={l}>{l}</li>)}</ul>
			</div>
		)
	}
}

打开浏览器可以看到对应的界面:

通过 update 操作可以看到视图的更新,同时打开开发者工具也能够看到影响DOM的闪动是符合我们预期的。

created time in 6 days

startedFEGuideTeam/FEGuide

started time in 6 days

starteddmoosocool/puppeteer-autotest

started time in 6 days

startedlibra/libra

started time in 7 days

startedxcatliu/leetcode

started time in 7 days

startedjolaleye/cssfx

started time in 7 days

startedjdeal/qim

started time in 8 days

startedkolodny/immutability-helper

started time in 8 days

startedwangweianger/zanePerfor

started time in 8 days

startedbendrucker/screen-orientation

started time in 8 days

startedmkaz/lanyon

started time in 9 days

startedareslabs/alita

started time in 9 days

issue commenthuruji/JDS_weekly

2019第四周

css加载会造成阻塞吗?

webkit 渲染过程

https://user-gold-cdn.xitu.io/2018/9/3/1659db14e773f9cc?imageView2/0/w/1280/h/960/format/webp/ignore-error/1

Gecko渲染过程

https://user-gold-cdn.xitu.io/2018/9/3/1659db14e7df8a8f?imageView2/0/w/1280/h/960/format/webp/ignore-error/1

huruji

comment created time in 10 days

issue commenthuruji/JDS_weekly

2019第四周

js的类型转换:

https://github.com/jawil/blog/issues/1

https://camo.githubusercontent.com/a55d51af56d9187004bdca0c9fcc5b4ccb265bce/687474703a2f2f7777312e73696e61696d672e636e2f6c617267652f6136363063616232677931666379387173316b79736a3231646b313171343875

    1. undefined == null,结果是true。且它俩与所有其他值比较的结果都是false。
    1. String == Boolean,需要两个操作数同时转为Number。
    1. String/Boolean == Number,需要String/Boolean转为Number。
    1. Object == Primitive,需要Object转为Primitive(具体通过valueOf和toString方法)。
huruji

comment created time in 11 days

startedhttp2/http2-spec

started time in 11 days

startedhuyaocode/webKnowledge

started time in 11 days

startedlawler61/blog

started time in 11 days

issue openedhuruji/JDS_weekly

2019第四周

https://chrome.google.com/webstore/detail/performance-analyser/djgfmlohefpomchfabngccpbaflcahjf/related

利用 performance API 可视化展示网页性能数据的插件

image

created time in 12 days

startedmicmro/performance-bookmarklet

started time in 12 days

issue commenthuruji/blog

webpack 之 tree shaking

副作用是指是否影响其他模块,你所说的函数引用是指? @supperpandababy

huruji

comment created time in 12 days

startedsfyc23/EverydayWechat

started time in 13 days

startedcrimx/ext-saladict

started time in 13 days

startedSelection-Translator/crx-selection-translate

started time in 13 days

startedi5ting/how-brower-work-and-perfomace-tunning

started time in 13 days

startedlukeed/tinydate

started time in 13 days

issue openedhuruji/blog

ReferenceError 和 TypeError

ReferenceError 是作用域判别失败,TypeError 是作用域判断成功,对结果的操作非法或不合理

  • RHS:right-hand side
  • LHS:left-hand side

如果 RHS 查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出 ReferenceError 异常。值得注意的是,ReferenceError 是非常重要的异常类型。 相较之下,当引擎执行 LHS 查询时,如果在顶层(全局作用域)中也无法找到目标变量, 全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎,前提是程序运行在非 “严格模式”下。 “不,这个变量之前并不存在,但是我很热心地帮你创建了一个。” ES5 中引入了“严格模式”。同正常模式,或者说宽松 / 懒惰模式相比,严格模式在行为上 有很多不同。其中一个不同的行为是严格模式禁止自动或隐式地创建全局变量。因此,在 严格模式中 LHS 查询失败时,并不会创建并返回一个全局变量,引擎会抛出同 RHS 查询 失败时类似的 ReferenceError 异常。 接下来,如果 RHS 查询找到了一个变量,但是你尝试对这个变量的值进行不合理的操作, 比如试图对一个非函数类型的值进行函数调用,或着引用 null 或 undefined 类型的值中的 属性,那么引擎会抛出另外一种类型的异常,叫作 TypeError。 ReferenceError 同作用域判别失败相关,而 TypeError 则代表作用域判别成功了,但是对 结果的操作是非法或不合理的

created time in 13 days

startedpalmerhq/tsdx

started time in 13 days

issue commenthuruji/JDS_weekly

2019第3周

ES6 中 let 暂时性死区详解 https://segmentfault.com/a/1190000015603779

huruji

comment created time in 13 days

issue commenthuruji/JDS_weekly

2019第3周

https://www.jianshu.com/p/da8e9fba48b7

visibility :hidden和display:none的区别

huruji

comment created time in 13 days

issue commenthuruji/JDS_weekly

2019第3周

https://stackoverflow.com/questions/44596937/chrome-memory-cache-vs-disk-cache

chrome 资源 network 面板中的 from disk cache 和 from memory cache

huruji

comment created time in 15 days

startedeasonhuang123/blog

started time in 15 days

issue commenthuruji/JDS_weekly

2019第3周

http://lynnelv.github.io/js-event-loop-nodejs

huruji

comment created time in 16 days

started132yse/fre

started time in 16 days

issue commenthuruji/JDS_weekly

2019第3周

https://tc39.es/

es 官方

huruji

comment created time in 16 days

startedxd-tayde/easy-redux-react

started time in 17 days

startedxd-tayde/matting

started time in 17 days

startedkrausest/js-framework-benchmark

started time in 20 days

startedKieSun/react-interpretation

started time in 20 days

startedaermin/ghChat

started time in 20 days

fork huruji/Enterprise-Registration-Data-of-Chinese-Mainland

中国大陆 31 个省份1978 年至 2019 年一千多万工商企业注册信息,包含企业名称、注册地址、统一社会信用代码、地区、注册日期、经营范围、法人代表、注册资金、企业类型等详细资料。This repository is an dataset of over 10,000,000 enterprise registration data of 31 provinces in Chinese mainland from 1978 to 2019.【工商大数据】、【企业信息】、【enterprise registration data】。

fork in 21 days

startedreactos/reactos

started time in 22 days

issue commenthuruji/JDS_weekly

2019第3周

dom diff

https://zhuanlan.zhihu.com/p/20346379

https://zhuanlan.zhihu.com/p/36500459

https://www.zhihu.com/question/66851503/answer/246766239

http://www.ayqy.net/blog/react-list-diff/

huruji

comment created time in 22 days

startedCoinXu/Particle

started time in 22 days

issue openedhuruji/blog

编写一个 mini版 的 React(一)

jsx 语法

在 React 中一个 Node 节点会被描述为一个如下的 js 对象:

{
   type: 'div',
   props: {
      className: 'content'
   },
   children: []
}

这个对象在 React 中会被 React.createElement 方法返回,而 jsx 语法经过 babel 编译后对应的 node 节点就会编译为 React.createElement 返回,如下的 jsx 经过 babel 编译后如下:

const name = 'huruji'
const content = <ul className="list">
        <li>{name}</li>
        huruji
      </ul>
const name = 'huruji';
const content = React.createElement("ul", {
  className: "list"
}, React.createElement("li", null, name), "huruji");

从编译过后的代码大致可以得到以下信息:

  • 子节点是通过剩余参数传递给 createElement 函数的

  • 子节点包括了文本节点

  • 当节点的 attribute 为空时,对应的 props 参数为 null

为了加深对于这些的理解,我使用了 typescript 来编写,vdom 的 interface 可以大致描述如下,props 的 value 为函数的时候就是处理相应的事件:

interface VdomInterface {
	type: string
	props: Record<string, string | Function>
	children: VdomInterface[]
}

其中因为子节点其实还可以是文本节点,因此需要兼容一下,

export interface VdomInterface {
	type: string
	props: Record<string, string | Function>
	children: VdomType[]
}

type VdomType = VdomInterface | string

实际上,React 的声明文件对于每个不同的 HTML 标签的 props 都做了不同的不同的适配,对应的标签只能编写该标签下所有的 attributes,所以经常会看到以下这种写法:

type InputProps = React.InputHTMLAttributes<{}> & BasicProps;

export default class Input extends React.Component<InputProps, any> {
    // your comonent's code
}

这里一切从简,createElement 函数的内容就会是下面这个样子:

interface VdomInterface {
	type: string
	props: Record<string, string | Function>
	children: VdomInterface[]
}

export default function createElement(
	type: string,
	props: Record<string, string | Function>,
	...children: VdomType[]
): VdomType {
	if (props === null) props = {}
	console.log(type)
	debugger
	return {
		type,
		props,
		children
	}
}

测试

编写我们的测试,为了不需要再编写繁琐的 webpack 配置,我使用了 saso 作为这次打包的工具,创建目录目录结构:

--lib2
--src
   --index.html
   --index.tsx
   --App.tsx
--saso.config.js

因为 saso 推崇以 .html 文件为打包入口,所以在 .html 中需要指定 .index.ts 作为 script 属性 src 的值:

<script src="./index.tsx" async defer></script>

saso 配置文件 saso.config.js 配置一下 jsx 编译后的指定函数,内容如下:

module.exports = {
  jsx: {
    pragma: 'createElement'
  },
}

App.tsx 内容如下:

import { createElement } from '../lib2/index'

const name = 'huruji'
const content = (
	<ul className="list">
		<li>{name}</li>
		huruji
	</ul>
)

export default content

index.ts 内容如下:

import App from './App'
console.log(App)

在根目录中运行 saso dev,可以在控制台中看到打包编译完成,在浏览器中访问 http://localhost:10000 并打开控制台,可以看到组件 App 编译过后被转化为了一个 js 对象:

渲染真实 DOM

接下来就需要考虑如何将这些对象渲染到真实的 DOM 中,在 React 中,我们是通过 react-dom 中的 render 方法渲染上去的:

ReactDOM.render(<App/>, document.querySelector('#app'))

react 是在版本 0.14 划分为 reactreact-dom,react 之所以将 渲染到真实 DOM 单独分为一个包,一方面是因为 react 的思想本质上与浏览器或者DOM是没有关系的,因此分为两个包更为合适,另外一个方面,这也有利于将 react 应用在其他平台上,如移动端应用(react native)。

这里为了简单,就不划分了, 先写下最简单的渲染函数,如下:

export default function render(vdom:VdomType, parent: HTMLElement) {
  if(typeof vdom === 'string') {
    const node = document.createTextNode(vdom)
    parent.appendChild(node)
  } else if(typeof vdom === 'object') {
    const node = document.createElement(vdom.type)
    vdom.children.forEach((child:VdomType) => render(child, node))
    parent.appendChild(node)
  }
}

vdom 是字符串时对应于文本节点,其实这从 VdomType 类型中就可以看出来有 string 和 object 的情况(这也正是我喜欢 ts 的原因)。

index.tsx 中编写相应的测试内容:

import { render, createElement } from '../lib2'

render(
	<div>
		<p>name</p>
		huruji
	</div>,
	document.querySelector('#app')
)

这个时候可以看到,对应的内容已经渲染到 dom 中了

设置DOM属性

对于每个 dom 来说,除了普通的属性外,jsx 使用 className 来替代为 class,on 开头的属性作为事件处理,style是一个对象,key 属性作为标识符来辅助 dom diff,因此这些需要单独处理,key属性我们存储为 __key, 如下:

export default function setAttribute(node: HTMLElement & { __key?: any }, key: string, value: string | {} | Function) {
	if (key === 'className') {
		node['className'] = value as string
	} else if (key.startsWith('on') && typeof value === 'function') {
		node.addEventListener(key.slice(2).toLowerCase(), value as () => {})
	} else if (key === 'style') {
		if (typeof value === 'object') {
			for (const [key, val] of Object.entries(value)) {
				node.style[key] = val
			}
		}
	} else if (key === 'key') {
		node.__key = value
	} else {
		node.setAttribute(key, value as string)
	}
}

修改对应的测试,如下:

import { render, createElement } from '../lib2'

render(
	<div className="list" style={{ color: 'red' }} onClick={() => console.log('click')}>
		<p key="123" style={{ color: 'black' }}>
			name
		</p>
		huruji
	</div>,
	document.querySelector('#app')
)

打开浏览器可以看到已经生效:

组件 Component

首先先修改测试内容,将 dom 移到 App.tsx 中,index.tsx 内容修改为:

import { render, createElement } from '../lib2'
import App from './App'

render(<App />, document.querySelector('#app'))

打开浏览器可以看到这个时候报错了:

其实这个错误很明显,就是这个时候 Content 组件编译后传给 createElement 函数的第一个参数是一个 vdom 对象,但是我们并没有对 type 是对象的时候做处理,因此需要修改一下 createElement

export default function createElement(
	type: string | VdomType,
	props: Record<string, string | Function>,
	...children: VdomType[]
): VdomType {
	if (props === null) props = {}
	if (typeof type === 'object' && type.type) {
		return type
	}
	return {
		type: type as string,
		props,
		children
	}
}

这个时候就正常了。

先新建一个 Component 对象:

export default class Component {
  public props

  constructor(props) {
    this.props = props || {}
  }

}

对于 class Component 的写法,转化过后的传递给 createElement 的第一个参数就是一个以 React.Component 为原型的函数:

class Content extends React.Component {

  render(){
    return <div>content</div>
  }
}

const content = <div><Content name="huruji"/></div>
class Content extends React.Component {
  render() {
    return React.createElement("div", null, "content");
  }

}

const content = React.createElement("div", null, React.createElement(Content, {
  name: "huruji"
}));

也就是说这个时候 type 是一个函数,目前在 createElement 中和 render 中并没有做处理。所以肯定会报错。

在编写 class 组件的时候,我们必须要包含 render 方法,并且如果编写过 ts 的话,就知道这个 render 方法是 public 的,因此肯定需要实例化之后再调用 render 方法,我们放在 render 方法处理。Component 的 interface 可以表示为:

export interface ComponentType {
  props?: Record<string, any>
  render():VdomType
}

render 方法中单独处理一下 type 为 function 的情况:

const props = Object.assign({}, vdom.props, {
      children: vdom.children
    })
    const instance = new (vdom.type)(props)
    const childVdom = instance.render()
    render(childVdom, parent)
}

这里做的事情就是实例化后调用 render 方法。

这个时候,整个 render 方法的内容如下:

export default function render(vdom:VdomType, parent: HTMLElement) {
  if(typeof vdom === 'string') {
    const node = document.createTextNode(vdom)
    parent.appendChild(node)
  } else if(typeof vdom === 'object' && typeof vdom.type === 'string') {
    const node = document.createElement(vdom.type)
    vdom.children.forEach((child:VdomType) => render(child, node))
    for(const prop in vdom.props) {
      setAttribute(node, prop, vdom.props[prop])
    }
    parent.appendChild(node)
  } else if (typeof vdom === 'object' && typeof vdom.type === 'function') {
    const props = Object.assign({}, vdom.props, {
      children: vdom.children
    })
    const instance = new (vdom.type)(props)
    const childVdom = instance.render()
    render(childVdom, parent)
  }
}

修改我们的测试内容:

import { render, createElement, Component } from '../lib2'

class App extends Component {
	constructor(props) {
		super(props)
	}

	render() {
		const { name } = this.props
		debugger
		return <div style={{ color: 'red', fontSize: '100px' }}>{name}</div>
	}
}

render(<App name={'app'} />, document.querySelector('#app'))

打开浏览器,可以看到内容已经被正常渲染出来了:

处理 Functional Component

我们将测试内容修改为函数式组件:

function App({ name }) {
	return <div style={{ color: 'red', fontSize: '100px' }}>{name}</div>
}

这个时候可以看到报错:

这个错误是显而易见的,render 里将 Functional Component 也当成了 Class Component 来处理,但是 Functional Component 里并没有 render 属性,因此我们仍然需要修改,Class Component 的原型是我们定义的 Component ,我们可以通过这个来区分。

先增加一下 interface ,这能帮助我们更好地理解:

export interface ClassComponentType {
  props?: Record<string, any>
  render():VdomType
}

export type FunctionComponent = (props:any) => VdomType

export interface VdomInterface {
	type: FunctionComponent | string  | {
		new(props:any): ClassComponentType
	}
	props: Record<string, string | Function>
	children: VdomType[]
}

将 type 为 function 的逻辑修改为:

    const props = Object.assign({}, vdom.props, {
      children: vdom.children
    })

    let childVdom = null

    if(Component.isPrototypeOf(vdom.type)) {
      const vnode = vdom.type as {new(props:any): ClassComponentType}
      const instance = new (vnode)(props)
      childVdom = instance.render()

    } else {
      const vnode = vdom.type as FunctionComponent
      childVdom = vnode(props)
    }
    render(childVdom, parent)

这个时候整个 render 的内容如下:

import { VdomType, FunctionComponent } from './createElement'
import setAttribute from './setAttribute'
import Component, { ComponentType, ClassComponentType }  from './component'

export default function render(vdom:VdomType, parent: HTMLElement) {
  if(typeof vdom === 'string') {
    const node = document.createTextNode(vdom)
    parent.appendChild(node)
  } else if(typeof vdom === 'object' && typeof vdom.type === 'string') {
    const node = document.createElement(vdom.type)
    vdom.children.forEach((child:VdomType) => render(child, node))
    for(const prop in vdom.props) {
      setAttribute(node, prop, vdom.props[prop])
    }
    parent.appendChild(node)
  } else if (typeof vdom === 'object' && typeof vdom.type === 'function') {
    const props = Object.assign({}, vdom.props, {
      children: vdom.children
    })

    let childVdom = null

    if(Component.isPrototypeOf(vdom.type)) {
      const vnode = vdom.type as {new(props:any): ClassComponentType}
      const instance = new (vnode)(props)
      childVdom = instance.render()

    } else {
      const vnode = vdom.type as FunctionComponent
      childVdom = vnode(props)
    }
    render(childVdom, parent)
  }
}

这个时候重新打开一下浏览器,可以发现能够正常渲染了:

优化 render

目前 render 方法里渲染的节点包括:普通的文本节点、普通的标签节点、functional component、class component,但是个人感觉好像有点乱,在 render 方法中并没有反映我们的意图。

仔细回想一下 createElement 函数,除了文本节点外,其他类型的节点都会经过这个函数处理,我们其实可以在这里动动手脚,标记下节点的类型。

export default function createElement(
	type: VdomType | Vdom,
	props: Record<string, string | Function>,
	...children: Vdom[]
): Vdom {
	let nodeType:nodeType = 'node'
	if (props === null) props = {}

	if (typeof type === 'object' && type.type) {
		return type
	}

	if (typeof type === 'function') {
		if (Component.isPrototypeOf(type)) {
			nodeType = 'classComponent'
		} else {
			nodeType = 'functionalComponent'
		}
	}


	return {
		type: type as VdomType,
		props,
		children,
		nodeType
	}
}

这个时候重写下 render 方法会更加清晰:

import { Vdom } from './types/vdom'

import setAttribute from './setAttribute'
import { ClassComponent, FunctionComponent } from './types/component';

export default function render(vdom:Vdom, parent: HTMLElement) {

  if(typeof vdom === 'string') {
    const node = document.createTextNode(vdom)
    parent.appendChild(node)
    return
  }


  switch(vdom.nodeType) {
    case 'node':
      const node = document.createElement(vdom.type as string)
      vdom.children.forEach((child:Vdom) => render(child, node))
      for(const prop in vdom.props) {
        setAttribute(node, prop, vdom.props[prop])
      }
      parent.appendChild(node)
      break;
    case 'classComponent':
      const classProps = Object.assign({}, vdom.props, {
        children: vdom.children
      })
      const classVnode = vdom.type as {new(props:any): ClassComponent}
      const instance = new (classVnode)(classProps)
      const classChildVdom = instance.render()
      render(classChildVdom, parent)
      break
    case 'functionalComponent':
      const props = Object.assign({}, vdom.props, {
        children: vdom.children
      })
      const vnode = vdom.type as FunctionComponent
      const childVdom = vnode(props)
      render(childVdom, parent)
      break
    default:
  }

}

created time in 23 days

push eventhuruji/saso

huruji

commit sha 2108084f8fca706b06ba195880adeb52a4eebe1e

fix: :bug: tsx => jsxpragma

view details

huruji

commit sha b24d5062a0dbd8f6af542cb3ed9aab2133acda0d

feat: :fire: remove console

view details

huruji

commit sha 7b78c40ece28b3fcc788220fab5e8297776559f4

chore(release): publish - saso@3.7.1

view details

push time in 24 days

created taghuruji/saso

tagsaso@3.7.1

Zero config bundler to help you to build fantastic APP

created time in 24 days

startediamdustan/react-hardware

started time in 25 days

startedofirdagan/build-your-own-react

started time in 25 days

startedplotly/dash

started time in 25 days

issue commenthuruji/JDS_weekly

2019第3周

https://www.leevii.com/2018/10/record-in-typescript.html

ts中的Record

huruji

comment created time in a month

startedlihongxun945/myblog

started time in a month

startedjustadudewhohacks/opencv4nodejs

started time in a month

startedandreupifarre/preload-it

started time in a month

push eventhuruji/assets-preloader

huruji

commit sha 5f5e66b1aea3a72711cae7acdd88fe5b6f524195

docs: :pencil: add statement

view details

push time in a month

issue openedhuruji/blog

React 条件渲染方法大全

uJICFgUHyGU

使用 if else

import React from 'react'

interface Feed {
	name: string
	price: string
}

const FeedList: React.FunctionComponent<{ feeds: Feed[] }> = props => {
	const { feeds } = props

	if (feeds.length) {
		return <div>have no data</div>
	} else {
		return (
			<div>
				{feeds.map(item => (
					<p>
						{item.name}:{item.price}
					</p>
				))}
			</div>
		)
	}
}

使用 switch

const Content: React.FunctionComponent<any> = ()=> {
  const [style, setStyle] = useState<number>(0)

	return (
		<div>
			{(() => {
			  switch (style) {
					case 1:
						return 'black'
						break
					case 2:
						return 'pink'
						break
					default:
						return 'white'
				}
			})()}
		</div>
	)
}

使用三元运算法

const FeedList: React.FunctionComponent<{ feeds: Feed[] }> = props => {
	const { feeds } = props

	return (
		<div>
			{feeds.length
				? 'have no data'
				: feeds.map(item => (
						<p>
							{item.name}:{item.price}
						</p>
				  ))}
		</div>
	)
}

使用逻辑 && 运算法

当渲染不是非一即二的时候,而只是在某一情况下渲染时,使用三元运算法就会出现 nullundefinedfalse、空字符串之类的值,这在我看来好像有点奇怪,有点不必要

const UserInfo: React.FunctionComponent<InfoProps> = props => {
  const { name } = props

  return (
		<div>
      { name ? <p>name</p> : null }
		</div>
	)
}

这个时候或许使用逻辑运算符会是更好的选择:

const UserInfo: React.FunctionComponent<InfoProps> = props => {
	const { name } = props

	return (
		<div>
      { name && <p>name</p> }
		</div>
	)
}

使用枚举

我们经常使用js对象 key...value 的形式存储内容,使用 . 操作符来取的相应的值(这是一种hash表),如果我们将这个与 react component 结合就会发生奇妙的反应:

const APP: React.FunctionComponent<any> = ()=> {
  const [type, setStyle] = useState<string>('noData')
	return (
		<div>
		{
		{
                    networkError: <div>some error with your network</div>,
                    noData: <div>have no data</div>,
                    success: <div>have data</div>
                }[type]
		}
		</div>
	)
}

可能将这个 value 为 React Component 的对象单独抽出来或许更方便理解

const APP: React.FunctionComponent<any> = ()=> {
  const [type, setStyle] = useState<string>('noData')
  const content = {
    networkError: <div>some error with your network</div>,
    noData: <div>have no data</div>,
    success: <div>have data</div>
  }
	return (
		<div>
			{content[type]}
		</div>
	)
}

当我初次接触到这种写法的时候,最直观的感觉就是这就是 switch 的替代(alternate),这就是多种状态下的优雅编程。

但是,我很好奇,这是怎么运行的,我在命令行中编写类似的代码:

{a: 12, b:13}['a']

如我所料,这应该会报错,是一种语法错误:

那么为什么在 jsx 中却是可以正确运行,why?

我思索再三,jsx 组件会被 babel 转化为 React.createElement 这类的函数,如下:

其实本质上我们的枚举值会成为函数 React.createElement 的一个参数,那么这个参数就会运行一次拿到表达式的返回值,那么就是等于

({a: 12, b:13}['a'])

不出所料,这正常运行了,并拿到了相应的值。

这个形式非常类似于我们初学 JavaScript 时的匿名自执行函数:

我们还被告诉,如果我们不关心函数返回值,我们可以在函数表达式上前加上一元操作符同样生效,同理运用在我们这里同样生效,虽然我们这里显然需要拿到返回值,但是至少不报错了:

why?仅仅是 JavaScript 解析器解析语法的规则?原谅我不是科班出身,对编程语言底层不了解,但我对此非常感兴趣,如果有知道的同学期待您能告诉原因,或者告诉我应该学习什么来获取答案。

扯的有点远了,但是记住,枚举值是一种非常优秀的条件渲染模式。

使用高阶组件

这是一个摘自 robinwieruch 博客的例子:

// HOC declaration
function withLoadingIndicator(Component) {
  return function EnhancedComponent({ isLoading, ...props }) {
    if (!isLoading) {
      return <Component { ...props } />;
    }

    return <div><p>Loading...</p></div>;
  };
}

// Usage
const ListWithLoadingIndicator = withLoadingIndicator(List);
<ListWithLoadingIndicator
  isLoading={props.isLoading}
  list={props.list}
/>

我对于高阶组件用的不多,但是很明显的一点就是如果你封装了一个公共组件,那么这种模式很有用。

使用 do 表达式

do 表达式现在是在第一阶段,仓库在 https://github.com/tc39/proposal-do-expressions ,因此使用时需要添加相应的 babel plugin:@babel/plugin-proposal-do-expressions

这个插件最终会将其转化为三元操作符,如下:

以上就是我使用过的条件渲染方法,希望对你有用,欢迎补充~

最后是一个广告贴,最近新开了一个分享技术的公众号,欢迎大家关注👇

created time in a month

startedmetafizzy/zdog

started time in a month

startedFunctionScript/FunctionScript

started time in a month

push eventhuruji/saso

huruji

commit sha 42945ad2a0ae28ee0211be1f7d24ab68ba5994f1

feat: :sparkles: add remove propstype plugin

view details

huruji

commit sha 125182328f17ecd5b4c5651144e3fe8a3ba8dc5a

chore(release): publish - saso@3.7.0

view details

push time in a month

created taghuruji/saso

tagsaso@3.7.0

Zero config bundler to help you to build fantastic APP

created time in a month

created taghuruji/saso

tagsaso@3.6.0

Zero config bundler to help you to build fantastic APP

created time in a month

push eventhuruji/saso

huruji

commit sha b0bf95a22a3f9e73a296756c52b1c7c0baef9530

fix: :bug: resolve-url-loader no debug

view details

huruji

commit sha eefc722720e8dfe4f24eea74e268b88220749c97

chore(release): publish - saso@3.6.0

view details

push time in a month

push eventhuruji/saso

huruji

commit sha 043fde8d0a1c111c7188b607368beb535b088930

fix: :bug: use npm replace legos

view details

huruji

commit sha 45b576f74c12b352c43c3b592f175d18c4d361d6

chore(release): publish - saso-template-mini-lib-ts@1.2.1

view details

push time in a month

created taghuruji/saso

tagsaso-template-mini-lib-ts@1.2.1

Zero config bundler to help you to build fantastic APP

created time in a month

created taghuruji/saso

tagsaso@3.5.3

Zero config bundler to help you to build fantastic APP

created time in a month

push eventhuruji/saso

huruji

commit sha 8ae49c6e5bf1dd0330a40a6eace1f026349f5d56

fix: :bug: sass file url rewrite

view details

huruji

commit sha 7549d7cd8bc8e0808ee509737b2334a76fd9e313

chore(release): publish - saso@3.5.3

view details

push time in a month

push eventhuruji/saso

huruji

commit sha 1a812cf918ea9ad9fa316fe40193b051449ebaed

chore(mini-lib-ts): null check

view details

huruji

commit sha 26462973d378dd397a982617f911968db665457c

chore(release): publish - saso-template-mini-lib-ts@1.2.0

view details

push time in a month

created taghuruji/saso

tagsaso-template-mini-lib-ts@1.2.0

Zero config bundler to help you to build fantastic APP

created time in a month

issue openedhuruji/blog

命令式调用你的 React web 组件

大多数时候,是这样使用 React 组件的:

通过显示地修改传递给组件的 props 来改变组件的状态(显隐、颜色、内容等),这在大多数时候非常好用,并且这就是 React 的设计哲学,这属于声明式调用,但考虑一下我们经常用到的弹框组件(toast、modal、loading),因为弹框组件会被经常调用,并且可能同时存在多个实例,这样就需要维护多个props的状态,同时还需要手动修改父组件的 state 来控制显隐,这是非常繁琐的。

类似于 antd 的message 的使用方式 message.info('This is a normal message') ,antd 称其为全局提示组件。

这种调用方式就是命令式调用,这种调用方式非常类似于以往 jQuery 时代的js库,好处在于不再需要不断地维护 组件 的状态,这对于 toastmodalloading 来说非常有用。

其实设计这样一个组件并不难,其核心在于主动调用 ReactDOM.render 方法,将组件渲染上去,以设计一个 toast info 组件为例:

首先编写普通的 react 组件:

import React from 'react'
import ReactDOM from 'react-dom'

export interface InfoProps {
	/**
	 * 提示消息
	 */
	msg: string
	/**
	 * 图标类型,none,info,fail,success
	 * 默认为 none
	 */
	icon?: string
}

export default class Info extends React.Component<InfoProps, any> {
	public defaultProps: InfoProps = {
		msg: '',
		icon: 'none'
	}

	render() :React.ReactNode{
		const { icon, msg } = this.props
		const showIcon = icon !== 'none'
		const iconClass = `icon ${icon !== 'info' ? 'icon_' + icon : ''}`
		return (
			<div className="mod_alert_v2 show fixed">
				{showIcon ? <i className={iconClass} /> : null}
				<p>{msg}</p>
			</div>
		)
	}
}


暴露一个普通方法,来手动挂载到页面上去

import React from 'react'
import ReactDOM from 'react-dom'
import Info, { InfoProps } from './Info'

export default function info(
	opts: InfoProps & {
		/**
		 * 计时消失毫秒数,默认 2000
		 */
		delay?: number
		/**
		 * 是否显示蒙层
		 * 默认不显示
		 */
		showcoverdiv?: boolean
	} = {
		msg: ''
	}
) {
	const options = Object.assign(
		{
			msg: '',
			delay: 2000,
			icon: 'none',
			showcoverdiv: false
		},
		opts
	)

	const div = document.createElement('div')
	document.body.appendChild(div)
	ReactDOM.render(<Info msg={options.msg} icon={options.icon} />, div)

	setTimeout(() => {
		ReactDOM.unmountComponentAtNode(div)
		document.body.removeChild(div)
	}, options.delay)
}

其核心就是通过 ReactDOM.render 来挂载上去的,通过 ReactDOM.unmountComponentAtNode 来卸载组件的。

简单的调用方式如下:

通过这种方式,可以很快地封装好你的 React 命令式组件,以个人经验而言,对于 web 常用的 toastmodalloading 这三种组件应该是封装为这种组件会更加方便使用的。

created time in a month

created taghuruji/saso

tagsaso-template-mini-lib-ts@1.1.0

Zero config bundler to help you to build fantastic APP

created time in a month

push eventhuruji/saso

huruji

commit sha bc1563454d7558f3d251f44ddd592e5add13fe79

feat: :sparkles: add lintstaged husky

view details

huruji

commit sha dc0734da5daeb84da18d975dc67aece8287935cf

chore(release): publish - saso-template-mini-lib-ts@1.1.0

view details

push time in a month

push eventhuruji/saso

huruji

commit sha ceba02e8b8826a67b6d7b5342706d0bda1e9801b

chore(release): publish - saso@3.5.2

view details

push time in a month

created taghuruji/saso

tagsaso@3.5.2

Zero config bundler to help you to build fantastic APP

created time in a month

push eventhuruji/saso

huruji

commit sha 0f6747a0c444ea779ec8430c14420b69e44b4770

fix: :bug: tsx compile

view details

push time in a month

issue commentSamHwang1990/blog

浅谈npm 的依赖与版本

mark

SamHwang1990

comment created time in a month

push eventhuruji/saso

huruji

commit sha 6da793f108c88f1dcb30abec6c346b54eddbf834

chore: 🔧add publish script

view details

push time in a month

created taghuruji/saso

tagsaso-template-mini-lib-ts@1.0.0

Zero config bundler to help you to build fantastic APP

created time in a month

created taghuruji/saso

tagsaso@3.5.1

Zero config bundler to help you to build fantastic APP

created time in a month

push eventhuruji/saso

huruji

commit sha ac976adac1960309b48b4c39a0492dd420a4e542

fix: :bug: add less package

view details

huruji

commit sha 2b85b389d7257d8018374c86be7ffb206f2d087c

feat: ✨📦add mini lib ts template

view details

huruji

commit sha 0e71cb281aff7542cbb019a1332bb2b3a981908d

chore(release): publish - saso@3.5.1 - saso-template-mini-lib-ts@1.0.0

view details

push time in a month

startedluwes/sinuous

started time in a month

push eventhuruji/saso

huruji

commit sha eda4afff9b9ff8a73be4f1b13c84379b2065134a

feat: :sparkles: init command

view details

huruji

commit sha 56cde26dd9aebeffda0ba2e728ce30322c03838d

chore(release): publish - saso@3.5.0

view details

push time in a month

created taghuruji/saso

tagsaso@3.5.0

Zero config bundler to help you to build fantastic APP

created time in a month

push eventhuruji/leetcode

忽如寄

commit sha 7c60a127e53a879295761b4063d304500ebedf7d

Create index.js

view details

push time in a month

issue commenthuruji/blog

微信小程序开发的那些神坑

内嵌 H5 上传照片,选完点完成后直接退出到小程序首页,出现机型 vivo x9

暂时无解

huruji

comment created time in a month

startedbyterotate/Book

started time in a month

startedheshenxian1/OpenMindClass

started time in a month

startedmicrosoft/dts-gen

started time in a month

startedbfirsh/jsnes

started time in a month

issue openedhuruji/blog

lerna 发布失败后的解决方案

lerna 作为多个包依赖的管理解决方案,确实解决了很多痛点,我目前在工作和生活中已经大量使用了lerna,但同时也会遇到一些问题,发布失败后的问题是遇到比较频繁的问题,因此记录一下

lerna publish 主要做了以下几件事:

  • 检查从上一个 git tag 之后是否有提交,没有提交就会显示 No changed packages to publish 的信息,然后退出

  • 检查依赖了修改过的包的包,并更新依赖信息

  • 提交相应版本的 git tag

  • 发布修改的包及依赖它们的包

看上去非常理想,但是使用起来可能很蛋疼,往往出问题的可能就是最后一步,发布包的问题,有可能你的网络情况有问题,有可能你没有登录npm(包括公司内部的 registry ),如果你需要发布到指定的 registry 上,你可以在 lerna.json 上指定:

"command": {
    "publish": {
      "message": "chore(release): publish",
      "registry": "http://legos.wq.jd.com/legosv5/registry/"
    }
  },

我已经遇到了好几次这个问题,如下:

关于这个问题,社区也有相应的讨论,可以在 lerna 项目的issue中找到,如 5241894

对此,lerna 到目前为止并没有提供很好的解决方案。

你可以在你这次失败之后使用 from-git 参数,即lerna publish from-git

或者我们可以手动回退 git 到 release 之前的版本,并删除相应的 git tag,如下:

git reset --hard HEAD~1 && git tag -d $(git log --date-order --tags --simplify-by-decoration --pretty=format:'%d' | head -1 | tr -d '()' | sed 's/,* tag://g')

created time in a month

more