🐳 Imitation & learning& and combination & creating
yuxino / source Goto Github PK
View Code? Open in Web Editor NEW🌟 The source code analysis
🌟 The source code analysis
🐳 Imitation & learning& and combination & creating
程序员的三大浪漫,编译原理,图形学,操作系统。最近在弄图形学相关的东西,emmmm , 说不上图形学。。。其实也就是在弄canvas。吃饭的时候看到支付宝付款后有一个刮刮卡。于是就在想能不能做个类似的东西呢。
答案是当然可以啊。于是就弄了一个demo。弄完之后又在想可不可以弄一个react的刮刮卡组件呢。当然也可以。这篇文章将记录我怎么弄一个这样的组件。
弄组件之前。我们先看看刮刮卡效果的实现原理。demo的代码很短加上空格也不到60行。
先看第一段。创建一个Image标签。并且获取canvas元素。为之后要用到这两个东西作准备。
const img = new Image();
img.src = "http://7xqvgr.com1.z0.glb.clouddn.com/70ed7f087bf40ad17078e01b5d2c11dfabeccec4.gif";
const canvas = document.querySelector('canvas');
接下来就是监听图片的load事件。当load完成后开始做某些事情。
img.addEventListener('load', function(e) { // ... });
除此以外就没有其他的代码了。后面的篇幅讲的都是load完之后做的事情。所以会省略掉上面那段代码。只贴里面的核心逻辑。
完成刮刮卡效果的最主要的核心是通过compositing operation来控制组合图形。详细的资料可以在这里查到。还不错的中文博客。
在刮刮卡里面用到了compositing operation的destination-out
属性。
The existing content is kept where it doesn't overlap the new shape.
这是MDN对destination-out
的描述。但是这段话翻译过来还是蛮抽象,蛮难理解的的。这里我用我们肉眼可见的效果。可观察的结果来说就是使用了destination-out
的图形。每当新的图形如果叠加在这个图形上的话。会让这个图形和新的图形叠加的部分变透明。
这里我特意写了个demo展示了一下这个效果。如果你不理解我上面的文字描述可以看看。
如果知道了destination-out
给我们带来的效果。那么实现一个擦除效果就很简单了。我们只需要简单的在一个图层上叠图形就好了。叠的图形的首选当然是圆形。因为圆形比较像人的手指。圆的大小可以自己决定。我们唯一要运算的东西只有一个,那就是x,y坐标。这两个坐标就是鼠标在画布里面的坐标。
一般来说刮刮卡 当然是要有一种刮开的感觉。当手指(鼠标)按住然后滑动才开始刮。松开后就停止刮。要实现这个逻辑的话也是非常简单。我们只需要维护一个state就好了。在开始时(mousedown)的时候把state设置为true。在松开时(mouseup)的时候把state设置为false。移(划)动(mouseup)的时候根据state判断是否开始擦除。
总结一下。我们要做的事情就是实现擦除效果。处理手机和PC的事件。codepen的例子只处理了PC。所以在react-scrath中会把他补全。
我使用了nwb构建项目。我写过一篇怎么构建项目的文章。可以看这里了解一下。但是这一篇不会告诉你怎么使用nwb。
为了写一个可以给其他人使用的sratch组件。那么我们需要定义一些可以定制化的东西。
我定了一下react-sratch需要接收的一些prop和它们的默认值。其中width,height,ratio的大小都是px。
prop | desc | defaultValue |
---|---|---|
width | canvas的width | 300 |
height | canvas的height | 200 |
baseBg | 刮开后的图(可以是一个图片链接。也可以是一个颜色) | null |
coverBg | 覆盖在刮开后的图或者颜色之上的东东 | #000 |
ratio | 刮开的程度 | 0.8 |
ratioSize | 刮开的大小 | 30 |
callback | 到达刮开的程度后调用的回掉 | empty func |
import React, {Component} from 'react'
import t from 'prop-types'
export default class ReactScratch extends Component {
// ....
}
ReactScratch.propTypes = {
width: t.oneOfType([t.string,t.number]),
height: t.oneOfType([t.string,t.number]),
baseImg: t.string,
baseBg: t.string,
coverBg: t.string,
ratio: t.oneOfType([t.string,t.number]),
ratioSize: t.number,
callback: t.func
}
ReactScratch.defaultProps = {
width: 300,
height: 200,
baseImg: null,
baseBg: null,
coverBg: '#000',
ratio: .8,
ratioSize: 30,
callback: () => {}
}
首先我们设置一下canvas的大小并且把底图加到canvas上面吧。这里为了获得canvas的ref。用到了新的api-createRef()。可以了解一下。
const setSize = (node,props) => {
const { width, height } = props
node.width = width
node.height = height
}
const getBg = (props) => {
const isLink = /^https?:\/\//i
const { baseBg } = props
const ret = isLink.test(baseBg) ? `url(${baseBg})` : baseBg
return ret
}
export default class ReactScratch extends Component {
constructor (props) {
super(props)
this.canvasRef = React.createRef()
}
componentDidMount () {
const node = this.canvasRef.current
setSize(node,this.props)
}
render () {
return (
<canvas style={{
border: '1px solid black',
background: getBg(this.props),
backgroundSize: 'cover'
}}
ref={this.canvasRef} />
)
}
}
我这里放弃思考图片要怎么样。一律都是cover
。我也不管图片是什么结尾。反正是https或者http开头的我都当成是链接。
现在可以这样使用。传一个地址或者一个颜色(可以是rgb,hsl)。
const baseBg = "http://os33nc36m.bkt.clouddn.com/FiXqeVa9OaZHb7empMXZrETKte9F"
// ....
<ReactScratch baseBg={baseBg} />
现在看起来是这样子。这里用的图是博士和坂本。
好啊。设置完了这个。那就加一层遮罩(覆盖在上面的东东)吧。也就是coverBg。因为增加了一些api。整个index.js
看起来有一些臃肿。所以我把设置canvas的东西抽出来放在了utils.js
里面。重用了一下isLink
函数。
import React, {Component} from 'react'
import t from 'prop-types'
import { getBg, getContext2d, setSize, setCover } from './utils'
export default class ReactScratch extends Component {
constructor (props) {
super(props)
this.canvasRef = React.createRef()
}
componentDidMount () {
const node = this.canvasRef.current
setSize(node,this.props)
setCover(node,this.props)
}
render () {
return (
<canvas style={{
border: '1px solid black',
background: getBg(this.props),
backgroundSize: 'cover'
}}
ref={this.canvasRef} />
)
}
}
ReactScratch.propTypes = {
width: t.oneOfType([t.string,t.number]),
height: t.oneOfType([t.string,t.number]),
baseImg: t.string,
baseBg: t.string,
coverBg: t.string,
ratio: t.oneOfType([t.string,t.number]),
ratioSize: t.number,
callback: t.func
}
ReactScratch.defaultProps = {
width: 300,
height: 200,
baseImg: null,
baseBg: null,
coverBg: '#000',
ratio: .8,
ratioSize: 30,
callback: () => {}
}
/**
* @private check string is a link ?
* @param {*} str
*/
const isLink = str => /^https?:\/\//i.test(str)
/**
* @public get canvas context2d
* @param {*} ref
*/
const getContext2d = (ref) => {
const ctx = ref.getContext('2d')
return ctx
}
/**
* @public get base background of canvas
* @param {*} props
*/
const getBg = (props) => {
const { baseBg } = props
const ret = isLink(baseBg) ? `url(${baseBg})` : baseBg
return ret
}
/**
* @public set canvas size
* @param {*} node
* @param {*} props
*/
const setSize = (node, props) => {
const { width, height } = props
node.width = width
node.height = height
}
/**
* @private set cover img
* @param {*} ref
* @param {*} props
*/
const setCoverImg = (ref,props) => {
const { width, height, coverBg } = props
const ctx = getContext2d(ref,props)
const img = new Image()
img.src = coverBg
ctx.drawImage(img, width, height)
ctx.globalCompositeOperation = 'destination-out'
}
/**
* @private set conver background color
* @param {*} ref
* @param {*} props
*/
const setCoverBgColor = (ref,props) => {
const { coverBg, width, height } = props
const ctx = getContext2d(ref,props)
ctx.fillStyle = coverBg;
ctx.fillRect(0, 0, width, height)
ctx.globalCompositeOperation = 'destination-out'
}
/**
* @public switch to set cover background color or set cover img
* @param {*} ref
* @param {*} props
*/
const setCover = (ref, props) => {
const { coverBg } = props
isLink(coverBg) ? setCoverImg(ref, props)
: setCoverBgColor(ref, props)
}
export {
getBg,
getContext2d,
setSize,
setCover
}
好现在我们可以这样使用了。如果不加参数的话默认会是一个黑色的块。
我这里设置了深绿色。如果想设置图片也可以。只要传一个链接就好了。现在看起来会是这个样子。
<ReactScratch baseBg={baseBg} coverBg="#006060" />
drawCircle就是字面意思画一个圆。这个圆是我们要用到的擦除效果。接收x,y坐标和size。size的话我们用ratioSize就好啦。于是乎我在utils.js
里面加了一段。如果你跟着我的笔记走的话。不要忘记在export上把drawCircle暴露出去了噢。
/**
* @public draw a cirle in (x,y) postion
* @param {*} ref
* @param {*} x
* @param {*} y
* @param {*} size
*/
const drawCircle = (ref, x, y, size) => {
const ctx = getContext2d(ref)
ctx.beginPath();
ctx.arc(x, y, size, 0, Math.PI * 2);
ctx.fill()
}
为了测试这个功能是否如期运行。我们就直接在componentDidMount
里面加一段吧。目标是看见博士的脸。
// ...
import { getBg, getContext2d, setSize, setCover, drawCircle } from './utils'
// ...
componentDidMount () {
const node = this.canvasRef.current
setSize(node,this.props)
setCover(node,this.props)
const { width, height, ratioSize } = this.props
const size = 30
const x = 255
const y = 55
drawCircle(node, x, y, ratioSize)
}
看见了吗。
好看见了。说明一切如期运行着。那么我们把加进index.js
的部分都删掉吧。 感觉我们现在就差一个事件监听了。那么接下来我们会开始创建我们的事件监听。
emmmm 我们要做的事件监听无非就是两个。
手机端会麻烦一点但是还好。
一般来说刮刮卡 当然是要有一种刮开的感觉。当手指(鼠标)按住然后滑动才开始刮。松开后就停止刮。要实现这个逻辑的话也是非常简单。我们只需要维护一个state就好了。在开始时(mousedown)的时候把state设置为true。在松开时(mouseup)的时候把state设置为false。移(划)动(mouseup)的时候根据state判断是否开始擦除。
在最前面的最后一段我曾经说过这个。所以我们需要维护一个state。现在我把这个state弄出来了并把它叫做pressed
。同时我添加了两个方法。一个是onPressed
和onLoosen
,意思分别是按压和松开的时候.
export default class ReactScratch extends Component {
constructor (props) {
super(props)
this.canvasRef = React.createRef()
this.state = { pressed: false }
this.onPressed = this.onPressed.bind(this)
this.onLoosen = this.onLoosen.bind(this)
}
onPressed () {
this.setState({ pressed: true })
}
onLoosen () {
this.setState({ pressed: false })
}
// ....
}
以上就是现在代码看起来的样子。接下来我们要知道在画布的哪个位置画圆。
我们先把事件赋给canvas。并添加一个叫做onMove
的事件。这个事件会在我们按压移动的时候触发并且打印出坐标。
export default class ReactScratch extends Component {
constructor (props) {
super(props)
this.canvasRef = React.createRef()
this.state = { pressed: false }
this.onPressed = this.onPressed.bind(this)
this.onLoosen = this.onLoosen.bind(this)
this.onMove = this.onMove.bind(this)
}
onPressed () {
this.setState({ pressed: true })
}
onLoosen () {
this.setState({ pressed: false })
}
onMove (e) {
const { pressed } = this.state
if (pressed) {
const left = e.target.offsetLeft
const top = e.target.offsetTop
const x = e.pageX -left , y = e.pageY - top
console.log(`x: ${x}, y: ${y}`)
}
}
componentDidMount () {
const node = this.canvasRef.current
setSize(node,this.props)
setCover(node,this.props)
}
render () {
return (
<canvas style={{
border: '1px solid black',
background: getBg(this.props),
backgroundSize: 'cover'
}}
onMouseDown={this.onPressed}
onMouseUp={this.onLoosen}
onMouseMove={this.onMove}
ref={this.canvasRef} />
)
}
}
略微修改onMove
。并让canvas绑定事件。
onMove (e) {
const { pressed } = this.state
if (pressed) {
const { ratioSize } = this.props,
left = e.target.offsetLeft,
top = e.target.offsetTop,
pageX = e.pageX || e.targetTouches[0].pageX,
pageY = e.pageY || e.targetTouches[0].pageY,
x = pageX - left - ratioSize / 2,
y = pageY - top - ratioSize / 2
drawCircle(this.canvasRef.current, x, y, ratioSize)
}
}
render () {
return (
<canvas style={{
border: '1px solid black',
background: getBg(this.props),
backgroundSize: 'cover'
}}
onMouseDown={this.onPressed}
onMouseUp={this.onLoosen}
onMouseMove={this.onMove}
onTouchStart={this.onPressed}
onTouchEnd={this.onLoosen}
onTouchMove={this.onMove}
ref={this.canvasRef} />
)
}
现在的react-scratch已经可以实现擦除了。删掉border: '1px solid black'
因为这个不是必要的只是我在开发的时候想让这个canvas显眼一点才添加的。
手机端
PC端
Ok,那么整个组件的开发就到这里结束了。
才怪呢。ratio的callback还没有做呢。那么问题来了怎么获取ratio...
说实话我不知道怎么获取。但我觉得应该是计算覆盖在底图上的α指数(透明程度)有多少吧。看了lucky-card的源码确认了这点。感谢lucky-card !! 这里贴一下lucky-card的实现。
function _forEach(items, callback) {
return Array.prototype.forEach.call(items, function(item, idx) {
callback(item, idx);
});
}
function _calcArea(ctx, callback, ratio) {
var pixels = ctx.getImageData(0, 0, this.cWidth, this.cHeight);
var transPixels = [];
_forEach(pixels.data, function(item, i) {
var pixel = pixels.data[i + 3];
if (pixel === 0) {
transPixels.push(pixel);
}
});
if (transPixels.length / pixels.data.length > ratio) {
callback && typeof callback === 'function' && callback();
}
}
无耻的修改复制到utils.js
里面。
/**
* @public compute the ratio in canvas
* @param {*} ref
* @param {*} props
*/
const computeRatio = (ref, props) => {
const { width, height } = props,
pixels = getContext2d(ref).getImageData(0, 0, width, height),
transPixels = []
pixels.data.forEach((item, i) => {
const pixel = pixels.data[i + 3]
if (pixel === 0) {
transPixels.push(pixel)
}
})
return transPixels.length / pixels.data.length
}
改动一下index.js
里的onLoosen
onLoosen () {
const node = this.canvasRef.current,
{ ratio, callback } = this.props,
_ratio = computeRatio(node, this.props)
if (_ratio >= ratio) { callback() }
this.setState({ pressed: false })
}
理论上来说这个回调函数执行的次数应该是一次。但是这并不应该交由我来处理。应该给用户处理到底要执行多少次。
更改一下用法。加一个callback。让到达ratio的时候输出hello react scratch
。
<ReactScratch baseBg={baseBg}
coverBg="#006060"
callback={() => console.log('hello react scratch')}
/>
一切运行正常 。终于react-strach可以release v1.0.0了
虽然是个很小的组件。但是我认为笔记的描述程度有点过于多了。往后的笔记会看情况精简。
这个组件没有做测试,覆盖率很低。未来可能会补上。但是现在就到此为止吧。还有别的事情要做。
仓库地址: react-scratch 。欢(qiu qiu)迎(ni)给个star。
Snabbdom 是Vue使用到的virtual DOM库。最近在看Vue的源码所以先学习一下这个库的用法。
Snabbdom给自己的定位是简单,模块化,强大高性能的virtual dom库。
因为Virtual Dom非常棒。它允许我们根据应用程序的状态来表达应用程序的视图。但是现有的解决方案太臃肿、太慢、缺少特性、API偏向OOP或缺少我需要的特性
Snabbdom 由极其简单高性能和可扩展的内核(非常小大概只有200 SLOC)组成。它提供了一个模块化体系结构,通过定制模块为扩展提供了丰富的功能。为了保持核心的简单,所有非必要的功能都委托给了模块。
你可以随心所欲地制造你需要的Snabbdom !挑选、选择和定制您想要的功能。或者,您可以只使用默认扩展,获得一个具有高性能、小尺寸和下面列出的所有功能的虚拟DOM库。
var snabbdom = require('snabbdom');
var patch = snabbdom.init([ // Init patch function with chosen modules
require('snabbdom/modules/class').default, // makes it easy to toggle classes
require('snabbdom/modules/props').default, // for setting properties on DOM elements
require('snabbdom/modules/style').default, // handles styling on elements with support for animations
require('snabbdom/modules/eventlisteners').default, // attaches event listeners
]);
var h = require('snabbdom/h').default; // helper function for creating vnodes
var container = document.getElementById('container');
var vnode = h('div#container.two.classes', {on: {click: someFn}}, [
h('span', {style: {fontWeight: 'bold'}}, 'This is bold'),
' and this is just normal text',
h('a', {props: {href: '/foo'}}, 'I\'ll take you places!')
]);
// Patch into empty DOM element – this modifies the DOM as a side effect
patch(container, vnode);
var newVnode = h('div#container.two.classes', {on: {click: anotherEventHandler}}, [
h('span', {style: {fontWeight: 'normal', fontStyle: 'italic'}}, 'This is now italic type'),
' and this is still just normal text',
h('a', {props: {href: '/bar'}}, 'I\'ll take you places!')
]);
// Second `patch` invocation
patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state
Snabbdom的核心只提供最基本的功能。它被设计得尽可能简单,同时仍然快速和可扩展。
初始化,非常好用。自定义Module,可以拿来处理各种属性。class
,style
,可以自己扩展。
var patch = snabbdom.init([
require('snabbdom/modules/class').default,
require('snabbdom/modules/style').default,
]);
patch 是通过init
创建的。接受两个参数。第一个是表示当前视图的DOM元素或vnode。第二个是表示新的,需要更新视图的vnode。
第一个参数可以是VDOM也可以是真实的DOM。如果是真实的DOM的话,调用Patch会把第二个参数的NewVdom创建成真实的DOM。如果第一个参数是VDOM,那么调用的时候会高效的替换掉不同的地方。
oldVdom需要被传进来,因为Snabbdom把需要的信息存在了Vdom里面。这使得实现一个更简单、性能更高的体系结构成为可能。这也避免了创建新的oldVnode树。
patch(oldVnode, newVnode);
建议通过h
来创建元素。h
接收tag
或者selector
,一个可选的options,可选的数据对象和可选的子字符串或数组。
var h = require('snabbdom/h').default;
var vnode = h('div', {style: {color: '#000'}}, [
h('h1', 'Headline'),
h('p', 'A paragraph'),
]);
将DOM节点转换为虚拟节点。特别适合预先存在的服务器端生成的内容。
var snabbdom = require('snabbdom')
var patch = snabbdom.init([ // Init patch function with chosen modules
require('snabbdom/modules/class').default, // makes it easy to toggle classes
require('snabbdom/modules/props').default, // for setting properties on DOM elements
require('snabbdom/modules/style').default, // handles styling on elements with support for animations
require('snabbdom/modules/eventlisteners').default, // attaches event listeners
]);
var h = require('snabbdom/h').default; // helper function for creating vnodes
var toVNode = require('snabbdom/tovnode').default;
var newVNode = h('div', {style: {color: '#000'}}, [
h('h1', 'Headline'),
h('p', 'A paragraph'),
]);
patch(toVNode(document.querySelector('.container')), newVNode)
snabbdom 提供了很多的hook方便我们做自己爱做的事情。
Hook的名字 | 触发时间 | 回调参数 |
---|---|---|
pre | 开始patch | none |
init | vnode被添加的时候 | vnode |
create | 已经基于vnode创建了一个DOM元素 | |
emptyVnode, vnode | ||
insert | 元素被插入到DOM里面 | vnode |
prepatch | patch完成之前 | oldVnode, vnode |
update | 元素被更新的时候 | oldVnode, vnode |
postpatch | patch完成之后 | oldVnode, vnode |
destroy | 元素被直接或间接移除 | vnode |
remove | 元素被直接或间接从DOM移除 | vnode, removeCallback |
post | patch 完成的时候触发 | none |
使用方法
h('div.row', {
key: movie.rank,
hook: {
insert: (vnode) => { movie.elmHeight = vnode.elm.offsetHeight; }
}
});
我们可以创建一个Modulel来处理Hook
var myModule = {
create: function(oldVnode, vnode) {
// invoked whenever a new virtual node is created
},
update: function(oldVnode, vnode) {
// invoked whenever a virtual node is updated
}
};
剩下的部分都是在讨论Module
的作用,这里我们不再深究,直接看Module源码也大概知道个所以然。大体上我们知道了Snabbdom怎么用和是做什么的已经够了,剩下的是继续研究VUE的代码了。
Vue参照Snabbdom自己实现了一个Patch。所以我们会发现Vue的Vdomw文件夹和Snabbdom基本上是大同小异的。但是会更加复杂添加了更多定制化的功能。
19年2月25日
今天出于好奇又重新看了一下算法。 其实算法的核心就是尽可能的利用相同标签的元素。通过四个索引找到一样的标签,对于正常的情况来说,效果还不错,但是对于极端的情况,乱序的标签,可能效果就不怎么好。很难找到可以重用的。Key的作用主要是位移元素。可以提升一些效率。
负责启动调试项目。
"scripts": {
"start": "yarn start:main-react16",
"start:main-react16": "npm-run-all --parallel build:watch start-child:* main-react16",
"start:main-vue2": "npm-run-all --parallel build:watch start-child:* main-vue2",
"start:main-vite": "npm-run-all --parallel build:watch start-child:* main-vite",
"main-react16": "cd dev/main-react16 && yarn start",
"main-vue2": "cd dev/main-vue2 && yarn start",
"main-vite": "cd dev/main-vite && yarn start",
"start-child:react16": "cd dev/children/react16 && yarn start",
"start-child:react17": "cd dev/children/react17 && yarn start",
"start-child:vue2": "cd dev/children/vue2 && yarn start",
"start-child:vue3": "cd dev/children/vue3 && yarn start",
"start-child:vite": "cd dev/children/vite && yarn start",
"start-child:angular11": "cd dev/children/angular11 && yarn start"
},
现在已经是 0.3.8 版本了,由于我 fork 的比较早 。。。于是乎看了一大段了没接着更新下来 。。 打算做完笔记再去看看最新版本的代码。首先阅读之前先看看这个吧。我另一个博客上的一些记录文案,聊了一下我对 swr 的一些理解。
先从核心的模块SizeAndPositionManager
说起。打开一下查看。不是很多400多行。
先看公用函数getItemSizeAndPositionManager
,看起来就是一个初始化的函数。
看完已经明白了如何初始化一个SizeAndPositionManager
了。主要的三个选项是itemCount
,itemSizeGetter
,
estimatedItemSize
分别对应了文档上提到的几个属性,estimated
是估算的意思,其他的都比较好理解,顺带一提这文件的源码其实来自react-virtualized
,也就是说看了这玩意约等于也看了一下react-virtualized
的源码。
回到这个函数里,itemCount
是100,estimatedItemSize
是15,itemSizeGetter
是一段函数,目前用意不明。看起来每次调用都会给itemSizeGetterCalls
增加东西。固定返回数字10(意义不明)。
更新: itemSizeGetterCalls
就是拿来计算真是高度的。estimatedItemSize
是预测高度。
这个函数会返回两个值sizeAndPositionManager
, itemSizeGetterCalls
。
这是第一个测试我们先看它。
这个测视里并没有用到itemSizeGetterCalls
,所以我们也不太好确定itemSizeGetterCalls
目前是做什么的。这个测试的目的是测试寻找最近元素用的。仔细看了一下注释并不是最近元素
而是nearest the specified offset
也就是最近偏移量的元素位置
。
初步结论
理解困惑之后
说白了这俩就是在找中间值。比如元素高10,目标位置是100那么最接近这个文素的就是第11个元素了,但是如果是101,那么第11个元素本身就是完美符合最接近偏移量的元素了。
初略看了一下代码实现,用到了二分(binarySearch)和指数查找(exponentialSearch)。准备之后去看看。
这块看起来蛮多的来研究一下。从名字来看是获取对应index的size和positon用的。
测试用例比较长,截图比较麻烦,一部分一部分来看。
噢噢噢,突然明白了,这个不是次数,是对应的 index 。因为访问的是 0 号,所以数组里面会有个 0。
那感觉上就会是访问一次加一条。。。我们试着多访问几次。。然鹅事实不是这样的。重复的访问只会有一条。
并且如果输入比较大的数字的话,比如3,就会变成[0,1,2,3]这样的结构。这应该要拜读源码才能理解了这个现象。感觉是为了获取正确的偏移量,需要按序的计算出之前元素的偏移量才能得出对应元素的偏移量。这个操作可能是感觉不是特别花时间,但是是可以优化的点。只要算过3,再获取2和1的时候就不需要重新计算了。不过整个算法复杂度看了一下也就O(n)吧。
下面的新的例子也印证了这一点。
优化测试。
好了本轮用例完结。
这是getSizeAndPositionForIndex衍生出来的东西,看起来是会把值缓存下来,记录元素对应的大小(size
)和元素的实际偏移量(offset
)。
显而易见的用例。下一个。
先看源码吧,这个东西源码比较短。
Total size of all items being measured.This value will be completedly estimated initially. As items as measured the estimate will be updated.
先说结论这句话有帮助么,没啥帮助,还是看源码和测试用例更能理解,先说结论 totalSize 算的是可滚动的总大小,以下简称总大小。
计算公式是: lastMeasuredSizeAndPosition.offset + lastMeasuredSizeAndPosition.size + (this.itemCount - this.lastMeasuredIndex - 1) * this.estimatedItemSize
简化一下。我们知道lastMeasuredSizeAndPosition
就是上个单测里的东西,表示最后测量元素
,包含size
和offset
两个属性。以下我们简称为lastOffset
和lastSize
。lastMeasuredIndex
就是最后测量元素的索引
,itemCount
就是列表的总数。estimatedItemSize
就是预计大小。
那么屡一下。
lastOffset + lastSize + (this.itemCount - this.lastIndex - 1) * estimatedItemSize
对应中文:
最后一个元素的偏移量 + 最后一个元素的大小 + (总数 - 最后一个元素的位置 - 1) * 预计大小
就是 总大小(totalSize)
了。
再简化一下:
总大小 = 最后一个元素的偏移量 + 最后一个元素的大小 + (剩余元素个数) * 预计大小
因为我们访问到最后元素的时候,说明我们此时渲染到了这个地方。也计算过前面的元素大小了。
因为这个特性我们可以推测,如果是动态算高度的性能会很差,因为必须真正的渲染出来再计算高度。但是真实情况如何,目前不清楚。
这又是一个非常大的测试块,说明这一块的功能很重要。
初始化项目依赖。
基座项目有:
子应用有:
"scripts": {
"bootstrap": "yarn && npm-run-all --serial install:*",
"install:main-react16": "cd dev/main-react16 && yarn",
"install:main-vue2": "cd dev/main-vue2 && yarn",
"install:main-vite": "cd dev/main-vite && yarn",
"install:child-react16": "cd dev/children/react16 && yarn",
"install:child-react17": "cd dev/children/react17 && yarn",
"install:child-vue2": "cd dev/children/vue2 && yarn",
"install:child-vue3": "cd dev/children/vue3 && yarn",
"install:child-vite": "cd dev/children/vite && yarn",
"install:child-angular11": "cd dev/children/angular11 && yarn",
},
npm-run-all ---> 运行所有脚本
对应的文件结构如下
micro-app/dev on dev
➜ tree . -L 2
.
├── children
│ ├── angular11
│ ├── angular14
│ ├── react16
│ ├── react17
│ ├── vite
│ ├── vue2
│ └── vue3
├── main-react16
│ ├── README.md
│ ├── config
│ ├── jest.config.js
│ ├── jsconfig.json
│ ├── mock
│ ├── node_modules
│ ├── package.json
│ ├── public
│ ├── src
│ ├── tests
│ └── yarn.lock
├── main-vite
│ ├── README.md
│ ├── index.html
│ ├── package.json
│ ├── public
│ ├── src
│ ├── tsconfig.json
│ ├── vite.config.ts
│ └── yarn.lock
└── main-vue2
├── README.md
├── babel.config.js
├── package.json
├── public
├── src
├── vue.config.js
└── yarn.lock
扫了下内容,基本都是模板项目。基座负责通过micro-app
引入子应用。路由形如这样:
const routes = [
{
path: '/',
redirect: '/react16/'
},
{
path: '/react16/*',
name: 'react16',
component: React16,
},
{
path: '/react17/*',
name: 'react17',
component: () => import(/* webpackChunkName: "react17" */ './pages/react17.vue'),
},
{
path: '/vue2/*',
name: 'vue2',
component: () => import(/* webpackChunkName: "vue2" */ './pages/vue2.vue'),
},
{
path: '/vue3/*',
name: 'vue3',
component: () => import(/* webpackChunkName: "vue3" */ './pages/vue3.vue'),
},
{
path: '/vite/*',
name: 'vite',
component: () => import(/* webpackChunkName: "vite" */ './pages/vite.vue'),
},
{
path: '/angular11/*',
name: 'angular11',
component: () => import(/* webpackChunkName: "angular11" */ './pages/angular11.vue'),
},
{
path: '/multiple/*',
name: 'multiple',
component: () => import(/* webpackChunkName: "multiple" */ './pages/multiple.vue'),
},
{
path: '/self/*',
name: 'self',
component: () => import(/* webpackChunkName: "self" */ './pages/self.vue'),
},
];
clsx 是什么
A tiny (228B) utility for constructing className strings conditionally.
Also serves as a faster & smaller drop-in replacement for the classnames module.
Dva-Loading 是一个处理Effect的插件。不过在Dva里面插件被理解为是一个Hook。因为Dva-Loading的文档解释的非常的不详细,于是看了一下源码,才恍然大雾。
Dva-loading通过extraReducers
和onEffect
两个Hook来给原来的Model加上了一个全局的loading
还有局部的loading
以及处理Hide
和Show
的Reducers。原理是给每个Effects
执行开始的时候和执行结束的时候加上一个Action
。大概看起来代码会是这样子。
function onEffect(effect, { put }, model, actionType) {
// 省略 ...
return function*(...args) {
yield put({ type: SHOW, payload: { namespace, actionType } });
yield effect(...args);
yield put({ type: HIDE, payload: { namespace, actionType } });
};
}
SHOW
和HIDE
会在extraReducers
里面被处理。
const extraReducers = {
[namespace](state = initialState, { type, payload }) {
const { namespace, actionType } = payload || {};
let ret;
switch (type) {
case SHOW:
ret = {
...state,
global: true,
models: { ...state.models, [namespace]: true },
effects: { ...state.effects, [actionType]: true },
};
break;
case HIDE: // eslint-disable-line
const effects = { ...state.effects, [actionType]: false };
const models = {
...state.models,
[namespace]: Object.keys(effects).some((actionType) => {
const _namespace = actionType.split('/')[0];
if (_namespace !== namespace) return false;
return effects[actionType];
}),
};
const global = Object.keys(models).some((namespace) => {
return models[namespace];
});
ret = {
...state,
global,
models,
effects,
};
break;
default:
ret = state;
break;
}
return ret;
},
};
global
会在有请求的时候被设置。其他的model
下的loading
会在请求执行的时候被设置为true。effects
是指定actionType的loading状态。
先从官网的Example研究。
Elements of equal height
。等高元素。例子里示意了这是包含了 100000 条元素的渲染。每一个的高度是50,所以wrapper的高度会是5e06。
当前页面只需要显示10条,所以外容器的高度会是 500。真正的DOM只渲染 10 个DOM。所有的元素的位置都是绝对定位。
都是通过 top 和 left 控制的。
由此大概可以推测出这个场景的优化是跟滚动条密切相关的。但是随之而来也会有一些需要思考的问题这么做会导致什么问题。
Variable heights
。 动态高度元素。第二种类型能够解决第一种提到的第一个问题。但是仍然存在交互的问题,不是长列表,他是长什么样子的。。
这次的高度计算方式说他是动态吧好像也不是。。。因为每个元素虽然高度不一样,但是看起来都是可确定的,现实世界反而是那种不可确定的更多,或者说我们要做长列表优化必须要把高度限制下来才能做么,带着这个问题我准备开始我的第一个实验。
先把项目克隆下来,可是有点阴间,跑不起来,因为环境问题,我只好开了个镜像 docker run -it -v $PWD:app -w /app node bash
。
看这个示例,基本可以断言连高度都得用户自己去想,这个库说不上非常舒服,我们必须被迫去适应库本身,定固定的高度去展示每一条数据,不过在大多数场景下,这个做法应该都是可以接受的。
为了证明是不是我对这个库有偏见,于是我去看了一下另一个热门库react-virtualized,发现也是基本一样的,确实是我对这个库有一点偏见了,看来要做长列表优化还是得牺牲一些东西的。
除此之外还加入了一些 feature, 但都不算是我的关注点,比如吸附布局,让一些条目固定的显示,这些感觉都算是比较好实现的。最核心的还是滚动的原理。不过这些相关的部分也是会看看的,毕竟跟代码也是息息相关的。
大的测试单元块有如下:
SizeAndPositionManager.test
:
这个模块看起来是最重要的模块,包含的测试是最多的。
目前这俩模块看起来还是不够清晰,SizeAndPositionManager
负责的职能较多已经超出了它的名字了(getVisibleRange
和getVisibleRange
),因为比较重要我打算先看这个文件先。
2020年12月31日16:42:00 确实不能动态算高度是反人类的事情。。但是可以通过高阶组件办到这件事情,这个算法并不在 react-tiny-virtual-list
里,需要通过react-virtualized-auto-sizer做到这点,后面会解析react-virtualized-auto-sizer
的源码。
话说还发现了vue
版本的感觉也不错。
又查了一下发现作者是个狠人。
原来react-sortable-hoc
也是他写的,有空会去研究一下的。
原标题是Vite 源码分析,但是我仔细想想,我感兴趣的只有一小部分是实现吧,毕竟实现并不是很复杂,大部分还是想从Vite里挖出来一些实际有用的东西,感受一下尤大的coding风格,思路。
日志处理之类的,参数处理之类的其实没多复杂。我们有时候并不需要 commander 之流。其实我觉得 commander 有点重 ..
`
Usage: vite [command] [args] [--options]
Commands:
vite Start server in current directory.
vite serve [root=cwd] Start server in target directory.
vite build [root=cwd] Build target directory.
Options:
--help, -h [boolean] show help
--version, -v [boolean] show version
--config, -c [string] use specified config file
--serviceWorker, -sw [boolean] configure service worker caching (default: false)
--port [number] port to use for serve
--open [boolean] open browser on server start
--base [string] public base path for build (default: /)
--outDir [string] output directory for build (default: dist)
--assetsDir [string] directory under outDir to place assets in (default: assets)
--assetsInlineLimit [number] static asset base64 inline threshold in bytes (default: 4096)
--sourcemap [boolean] output source maps for build (default: false)
--minify [boolean | 'terser' | 'esbuild'] enable/disable minification, or specify
minifier to use. (default: 'terser')
--mode, -m [string] specify env mode (default: 'development' for dev, 'production' for build)
--ssr [boolean] build for server-side rendering
--jsx ['vue' | 'preact' | 'react'] choose jsx preset (default: 'vue')
--jsx-factory [string] (default: React.createElement)
--jsx-fragment [string] (default: React.Fragment)
`
几个switch就搞定了主要的几个server
,build
之类的命令。minimist这个库属实太好用了。
const os = require("os");
const protocol = "https";
const port = "8080";
// 运行端口host打印
const interfaces = os.networkInterfaces();
// 处理 IPV4
Object.keys(interfaces).forEach((key) => {
(interfaces[key] || [])
.filter((details) => details.family === "IPv4")
.map((detail) => {
return {
type: detail.address.includes("127.0.0.1") ? "Local: " : "Network: ",
host: detail.address.replace("127.0.0.1", "localhost"),
};
})
.forEach(({ type, host }) => {
const url = `${protocol}://${host}:${port}/`;
console.log(` > ${type} ${url}`);
});
});
效果 Like this
这一段属于提炼出来的东西。可以这样轻松地给应用程序设置上环境变量。仅需加入到package.json
的scripts
里,通过set-env [xxx]
来修改管理环境变量。
const { exec } = require("child_process");
const dotenv = require("dotenv");
const env = {};
const mode = process.env.MODE;
const files = [".env", `.env.${mode}`];
files.forEach((file) => {
const { parsed } = dotenv.config({ path: `${__dirname}/${file}` });
Object.assign(env, parsed);
});
let keys = Object.keys(env);
keys = keys.map((key) => {
return `${key}=${env[key]}`;
});
exec(
`${keys.join(" ")} ${process.argv.splice(2).join(" ")}`,
(error, stdout, stderr) => {
if (error) {
console.error(`exec error: ${error}`);
return;
}
console.log(`stdout: ${stdout}`);
console.error(`stderr: ${stderr}`);
}
);
TODO: ///
这周决定宅着不出去做完平日里没做完的事情。其中之一就是分析redux的源码。我看的版本是v4.0.0
。Redux的作者是Dan Abramov。俄罗斯人。非常好的人,还回复过我的水文。
Redux本身代码量是很少的。典型的文档比代码多系列。测试也写得很全。Redux的标语是
Predictable state container for JavaScript apps
Predictable可以预测的状态管理容器。可预测最主要的就是可以做time machine,时间旅行。可以回滚到上次的状态。Redux对初学者来说可能是非常complex的。并且有些过于啰嗦。Redux本身不属于React的一部分。React-Redux和Redux是两个不同的项目。Redux本身只是一个很小的状态管理库。可以应用到其他的框架上。
看源码之前要做的事情就是摸熟这个框架或者codebase的用法。这样就不会觉得陌生。
首先要做的事情是从Gihub把Redux的源码克隆下来。
git clone [email protected]:reactjs/redux.git
克隆下来后为了调试代码。我自己写了个webpack.config.js,开个dev server配合vscode 的debugger in chrome进行调试。其实我也不知道正规的看代码方法吧(其实是找了很久没找到,又不想调试test case)。反正现在也不纠结,我就先这样。贴一下配置。
先进入redux根目录创建webpack.config.js
。再开个debugger目录。
touch webpack.config.js
mkdir debugger
装依赖
yarn add -D webpack
yarn add -D webpack-cli
yarn add -D html-webpack-plugin
yarn add -D webpack-dev-server
yarn
额 不做这一步也可以,只是我喜欢这样。然后弄完可以直接用vscode调试了。
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'development',
entry: './debugger/index.js',
devtool: 'inline-source-map',
output: {
path: __dirname + '/dist',
filename: 'index_debugger_bundle.js'
},
plugins: [
new HtmlWebpackPlugin({
template: './debugger/index.html'
})
]
}
<!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>
哭哭惹
</body>
</html>
import * as redux from '../src'
console.log(redux)
"scripts": {
"debugger": "webpack-dev-server --open --port=2333"
}
改一下vscode调试的配置就可以了。详细的自己看一下吧。source-map必须要开不然这插件就是shit。
以下是每个文件的用处。懒得打字所以一大部分都是抄袭的。贴上原文地址。因为原作者的版本比较旧了。所以自己补充了几个文红没写到的。
applyMiddlewar.js
使用自定义的 middleware 来扩展 Redux
bindActionCreators.js
把 action creators 转成拥有同名 keys 的对象,使用时可以直接调用
combineReducers.js
一个比较大的应用,需要对 reducer 函数 进行拆分,拆分后的每一块独立负责管理 state 的一部分
compose.js
从右到左来组合多个函数,函数编程中常用到
createStore.js
创建一个 Redux Store 来放所有的state
utils/warnimng.js
控制台输出一个警告,我们可以不用看
utils/actionTypes.js
redux内部使用的3个action
utils/isPlanObject.js
判断对象是不是纯对象
所有的一切都基于createStore创建出来的store。可以说是一切的开始。createStore
函数接受三个参数。
function createStore(reducer, preloadedState, enhancer)
createStore
暴露出的方法有以下这些
方法名 | 作用 |
---|---|
dispatch | dispatch一个action触发状态变化,调用reducer创建新的store |
subscribe | 添加listener状态改变的时候会被触发listener的回调函数 |
getState | 读取状态树管理的状态 |
replaceReducer | 替换当前使用的Reducer。如果你想要动态替换Reducer的话可以用到。 |
$$observable | 私有属性 不知道干嘛的 叫我看这个 |
createStore函数就是一个Observser实现的话也没啥好看的。读一读代码就能理解了。这里就不详细写了。不是很懂的地方有一个就是ensureCanMutateNextListeners
的作用。暂时我还没有理解。
redux实现middleware和compose函数有很多的关系。这个compose函数非常的简单。核心是这样。我删掉了一些不(特别)重要的代码。
const compose = (...funcs) => {
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
这个compose
只做一件事情。把函数组合起来。就是函数式编程的那个组合。返回一个从右到左执行的compose function
。比如传入(a,b,c)
这样的函数参数这样的执行顺序: c(b(a(...args)))
。我们可以做个实验,你可以直接把代码复制到控制台里面执行。
const a = () => console.log('a')
const b = () => console.log('b')
const c = () => console.log('c')
compose(a,b,c)()
输出的结果会是: c,b,a
。但是如果我们把这些函数做一个更加高阶的处理。让函数第一次执行的时候返回函数。变成这样的话。会发生一个很有趣的现象。
const a = ( next ) => ( param ) => { console.log(param); next('b'); }
const b = ( next ) => ( param ) => { console.log(param); next('c'); }
const c = ( next ) => ( param ) => { console.log(param); next('d'); }
const d = (param) => console.log(param)
const cp = compose(a,b,c)(d)
// execute
cp('a')
输出的结果会是a,b,c,d
。和之前的函数版本相比。此时的compose function
函数拥有了控制是否执行下一个函数的能力。并且通过调用next
来执行下一个。同时它变成正序的。Redux利用了这一特性。
const compose = (...funcs) => {
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args)
let dispatch = () => {
throw new Error(
`Dispatching while constructing your middleware is not allowed. ` +
`Other middleware would not be applied to this dispatch.`
)
}
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
Redux把store => next => action
的最外层通过执行middlewares.map(middleware => middleware(middlewareAPI))
传入参数把函数变成了next => action
的形式完成了中间件的模式。
combineReducer的作用是把零零散散的reducer拼起来或者说把复杂的单个reducer拆分成小的reducer。实现的原理都在代码上。以下代码做了一些精简的处理,去掉了一些不是特别重要的警告。
export default function combineReducers(reducers) {
// 获取reducers的key
const reducerKeys = Object.keys(reducers)
const finalReducers = {}
// 处理reducerKeys
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i]
// 非开发环境下 如果reducers[key]为undefined 抛出警告
if (process.env.NODE_ENV !== 'production') {
if (typeof reducers[key] === 'undefined') {
warning(`No reducer provided for key "${key}"`)
}
}
// 判断是否为方法
if (typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key]
}
}
// 获取finalReducerKeys的所有key
const finalReducerKeys = Object.keys(finalReducers)
// 组合state 原理是每次执行调用每个传过来的reducers
// 最终通过nextState拼出一个最大的state
// 通过`hasChanged`做了缓存处理
return function combination(state = {}, action) {
// 缓存处理
let hasChanged = false
const nextState = {}
// 调用每个reducer ..
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
const previousStateForKey = state[key]
const nextStateForKey = reducer(previousStateForKey, action)
// 如果nextStateForKey执行后的结果为undefined 说明该reducer返回的
// 结果是undefined 会抛出异常
if (typeof nextStateForKey === 'undefined') {
const errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
nextState[key] = nextStateForKey
// 和上次的状态比较 如果一致就为false。返回上一次的状态。
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
return hasChanged ? nextState : state
}
}
bindActionCreators
的最常见用的地方可能是在react-redux
调用mapDispatchToProps
的时候。
const TodoAction = (...args) => { type: 'TODO', { ...args } }
function mapDispatchToProps(dispatch) {
actions: bindActionCreators(TodoAction, dispatch)
}
不用bindActionCreators
的话会是这样写。就如下面的bindActionCreator
函数基本一致。要注意源码是没有s
那个。
调用有s
那个传一个对象。写起来会是这样。原理是遍历执行bindActionCreators
。
import * as TodoActions from '../actions/todo'
function mapDispatchToProps(dispatch) {
actions: bindActionCreators(TodoActions, dispatch)
}
源码如下:
function mapDispatchToProps(dispatch) {
actions: (...args) => {
dispatch(TodoActions(args))
}
}
源码如下:
function bindActionCreator(actionCreator, dispatch) {
return function() {
return dispatch(actionCreator.apply(this, arguments))
}
}
export default function bindActionCreators(actionCreators, dispatch) {
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}
if (typeof actionCreators !== 'object' || actionCreators === null) {
throw new Error(
`bindActionCreators expected an object or a function, instead received ${
actionCreators === null ? 'null' : typeof actionCreators
}. ` +
`Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?`
)
}
const keys = Object.keys(actionCreators)
const boundActionCreators = {}
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
const actionCreator = actionCreators[key]
if (typeof actionCreator === 'function') {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
}
return boundActionCreators
}
这个变量用来处理reducer 中 调用 dispatch,导致死循环 的情况。
如下代码,在reducer里dispatch 又被调用,这种情况redux是不允许的。
var reducer = function(state, action){
switch (action.type) {
case 'add_todo':
store.dispatch({type: 'yyy'}); // 调用B
return state;
default:
return state;
}
};
var store = redux.createStore(reducer, []);
store.dispacth({type: 'xxx'}); // 调用A
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.