记录一些平常看到的有趣的前端框架、知识。
sl1673495 / notes Goto Github PK
View Code? Open in Web Editor NEW记录一些配置笔记。
记录一些配置笔记。
记录一些平常看到的有趣的前端框架、知识。
出自这篇文章:
深入剖析 React Concurrent
创建子组件实例的过程发生在 _update
过程中。
调用栈大概变成这个样子了,创建子组件实例的过程发生在这个函数中(很容易被忽略)
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
简单来说如果是子组件,才会有自己的instance,然后上面就会 return。
具体的创建子组件实例的过程还是发生在子组件 vnode 的 init
生命周期里。
init: function init (vnode, hydrating) {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
var mountedNode = vnode; // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode);
} else {
// 这里
var child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
);
child.$mount(hydrating ? vnode.elm : undefined, hydrating);
}
},
createComponentInstanceForVnode
里会去真正的用 new 关键字构造出子组件的实例。
function createComponentInstanceForVnode (
vnode, // we know it's MountedComponentVNode but flow doesn't
parent // activeInstance in lifecycle state
) {
var options = {
_isComponent: true,
_parentVnode: vnode,
parent: parent
};
// check inline-template render functions
var inlineTemplate = vnode.data.inlineTemplate;
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render;
options.staticRenderFns = inlineTemplate.staticRenderFns;
}
return new vnode.componentOptions.Ctor(options)
}
进入了子实例的 this._init()
后,又会进入和上层组件一样的逻辑中。
生成完实例后,注意下面会调用子组件自身的 $mount
。
注意调用堆栈,这个过程发生在父组件的 beforeMount
和 mounted
之间。
这个系列的两篇
https://github.com/umijs/hooks
核心的库应该是father-doc
在每个目录下的index.md里通过<code src="./demo/index.tsx />引入组件,这样可以获得完整的代码展示。
https://codesandbox.io/s/compassionate-field-sy25t
当前版本的React 只有对于React内部的事件体系才是批量更新 在异步事件(如async函数)和用户手动监听的事件中每调用一次set方法 都会重新render视图
使用unstable_batchedUpdates可以批量更新
dan老哥说未来所有的更新都会是批量的
新版vscode需要配置这两个,才能做到编辑器中提示eslint报错,保存时自动修复
"eslint.validate": ["vue", "html", "javascript", "typescript"],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
集成过程可以参考这篇文章
https://dev.to/talohana/setup-your-typescript-project-with-eslint-prettier-and-lint-staged-25dg
主要是用于指定es modules语法的包,便于tree-shaking
在bable中设置 { module: false } 可以不把es modules转commonjs
参考资料:
https://segmentfault.com/a/1190000014286439
https://github.com/rollup/rollup/wiki/pkg.module
源码也很简单,就是把用户的update函数交给produce执行后返回给hook的set方法。
import produce, { Draft } from "immer";
import { useState, useReducer, useCallback } from "react";
export type Reducer<S = any, A = any> = (
draftState: Draft<S>,
action: A
) => void | S;
export function useImmer<S = any>(
initialValue: S | (() => S)
): [S, (f: (draft: Draft<S>) => void | S) => void];
export function useImmer(initialValue: any) {
const [val, updateValue] = useState(initialValue);
return [
val,
useCallback(updater => {
updateValue(produce(updater));
}, [])
];
}
export function useImmerReducer<S = any, A = any>(
reducer: Reducer<S, A>,
initialState: S,
initialAction?: (initial: any) => S
): [S, React.Dispatch<A>];
export function useImmerReducer(reducer, initialState, initialAction) {
const cachedReducer = useCallback(produce(reducer), [reducer]);
return useReducer(cachedReducer, initialState as any, initialAction);
}
function lazyLoadView (AsyncView) {
const AsyncHandler = () => ({
component: AsyncView,
// A component to use while the component is loading.
loading: require('./Loading.vue').default,
// A fallback component in case the timeout is exceeded
// when loading the component.
error: require('./Timeout.vue').default,
// Delay before showing the loading component.
// Default: 200 (milliseconds).
delay: 400,
// Time before giving up trying to load the component.
// Default: Infinity (milliseconds).
timeout: 10000
})
return Promise.resolve({
functional: true,
render (h, { data, children }) {
// Transparently pass any props or children
// to the view component.
return h(AsyncHandler, data, children)
}
})
}
const router = new VueRouter({
routes: [
{
path: '/foo',
component: () => lazyLoadView(import('./Foo.vue'))
}
]
})
原因在于 vue-router 本身是不支持 loading
占位组件的,所以要先渲染 lazyLoadView
返回的高阶组件,在组件内部再利用 vue 内部对于 loading
的支持去显示占位组件。
vue-router 和 vue 里都有一个方法去加载异步组件,叫做 resolveAsyncComponent
,但是他们的实现是不一致的,vue-router
只是单纯的等待 promise
被 resolve
之后,跳转路径,渲染组件。
首先响应式数据更新后,触发了渲染 Watcher 的回调函数vm._update(vm._render())
,
这段代码会带着新生成的 vnode去 触发 _update 函数。
然后触发 patch 过程,
对比旧新旧节点是否是相同类型的节点:
isSameNode
为false的话,直接销毁旧的 vnode,渲染新的 vnode。
会调用src/core/vdom/patch.js
下的patchVNode
方法。
就直接调用api把节点的直接替换掉文字内容就好,比如在 web 端就是 node.textContext = text
我们暂时把 vnode.chilren
也就是vnode的子节点称为 ch
说明是新增 ch,直接 addVnodes
添加新子节点。
说明是删除 ch,直接 removeVnodes
删除旧子节点
那么就是我们 diff算法
想要考察的最核心的点了,也就是新旧节点的 diff
过程。
通过
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
这些变量分别指向旧节点的首尾
、新节点的首尾
。
根据这些指针,对新旧节点的进行对比,其中某一项命中了,就直接进入 patch
节点的过程。
旧首节点和新首节点用 sameNode
对比。
旧尾节点和新首节点用 sameNode
对比
旧首节点和新尾节点用 sameNode
对比
旧尾节点和新尾节点用 sameNode
对比
如果以上逻辑都匹配不到,再利用key去对比。
然后不停的把匹配到的指针向内部收缩,直到新旧节点有一端的指针相遇(说明这个端的节点都被patch过了)。
在指针相遇以后,还有两种比较特殊的情况:
有新节点需要加入。
如果更新完以后,oldStartIdx > oldEndIdx
,说明旧节点都被 patch
完了,但是有可能还有新的节点没有被处理到。接着会走 addVnodes
去判断是否要新增子节点。
有旧节点需要删除。
如果新节点先patch完了,那么此时会走 newStartIdx > newEndIdx
的逻辑,那么就会走 removeVnodes
的逻辑去删除多余的旧子节点。
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
本文的重点就在于,这个
那么判断是否是相同节点的函数是 sameNode
,
function sameVnode (a, b) {
return (
a.key === b.key && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
)
)
)
}
可以看到,判断是否是 sameVnode
,传递给节点的 key
是关键。
假设我们有这样的一段代码:
<div id="app">
<ul>
<item
:key="index"
v-for="(num, index) in nums"
:num="num"
:class="`item${num}`"
></item>
</ul>
<button @click="change">改变</button>
</div>
<script src="./vue.js"></script>
<script>
var vm = new Vue({
name: "parent",
el: "#app",
data: {
nums: [1, 2, 3]
},
methods: {
change() {
this.nums.reverse();
}
},
components: {
item: {
props: ["num"],
template: `
<div>
{{num}}
</div>
`,
name: "child"
}
}
});
</script>
其实是一个很简单的列表组件,渲染出来 1 2 3
三个数字。我们先以 index
作为key,来跟踪一下它的更新。
我们接下来只关注 item
列表节点的更新,在首次渲染的时候,我们的虚拟节点列表 oldChildren
粗略表示是这样的:
[
{
tag: "item",
key: 0,
props: {
num: 1
}
},
{
tag: "item",
key: 1,
props: {
num: 2
}
},
{
tag: "item",
key: 2,
props: {
num: 3
}
}
];
在我们点击按钮的时候,会对数组做 reverse
的操作。那么我们此时生成的 newChildren
列表是这样的:
[
{
tag: "item",
key: 0,
props: {
+ num: 3
}
},
{
tag: "item",
key: 1,
props: {
+ num: 2
}
},
{
tag: "item",
key: 2,
props: {
+ num: 1
}
}
];
发现什么问题没有?key的顺序没变,传入的值完全变了。这会导致一个什么问题?
本来按照最合理的逻辑来说,旧的第一个vnode
是应该直接完全复用 新的第三个vnode
的,因为它们本来就应该是同一个vnode,自然所有的属性都是相同的。
但是在进行子节点的 diff
过程中,会在 旧首节点和新首节点用
sameNode 对比。
这一步命中逻辑,因为现在新旧两次首部节点
的 key
都是 0
了,
然后把旧的节点中的第一个 vnode
和 新的节点中的第一个 vnode
进行 patchVnode
操作。
这会发生什么呢?我可以大致给你列一下:
首先,正如我之前的文章props的更新如何触发重渲染?里所说,在进行 patchVnode
的时候,会去检查 props
有没有变更,如果有的话,会通过 _props.num = 3
这样的逻辑去更新这个响应式的值,触发 dep.notify
,触发子组件视图的重新渲染等一套很重的逻辑。
然后,还会额外的触发以下几个钩子,假设我们的组件上定义了一些dom的属性或者类名、样式、指令,那么都会被全量的更新。
而这些所有重量级的操作(虚拟dom发明的其中一个目的不就是为了减少真实dom的操作么?),都可以通过直接复用 第三个vnode
来避免,是因为我们偷懒写了 index
作为 key
,而导致所有的优化失效了。
<item
:key="Math.random()"
v-for="(num, index) in nums"
:num="num"
:class="`item${num}`"
/>
其实我听过一种说法,既然官方要求一个 唯一的key
,是不是可以用 Math.random()
作为 key
来偷懒?这是一个很鸡贼的想法,看看会发生什么吧。
首先 oldVnode
是这样的:
[
{
tag: "item",
key: 0.6330715699108844,
props: {
num: 1
}
},
{
tag: "item",
key: 0.25104533240710514,
props: {
num: 2
}
},
{
tag: "item",
key: 0.4114769152411637,
props: {
num: 3
}
}
];
更新以后是:
[
{
tag: "item",
+ key: 0.11046018699748683,
props: {
+ num: 3
}
},
{
tag: "item",
+ key: 0.8549799545696619,
props: {
+ num: 2
}
},
{
tag: "item",
+ key: 0.18674467938937478,
props: {
+ num: 1
}
}
];
可以看到,key
变成了完全全新的 3 个随机数。
上面说到,diff
子节点的首尾对比如果都没有命中,就会进入 key
的详细对比过程,简单来说,就是利用旧节点的 key -> index
的关系建立一个 map
映射表,然后用新节点的 key
去匹配,如果没找到的话,就会调用 createElm
方法 重新建立 一个新节点。
具体代码在这:
// 建立旧节点的 key -> index 映射表
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
// 去映射表里找可以复用的 index
idxInOld = findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
// 一定是找不到的,因为新节点的 key 是随机生成的。
if (isUndef(idxInOld)) {
// 完全通过 vnode 新建一个真实的子节点
createElm();
}
也就是说,咱们的这个更新过程可以这样描述:
123
-> 前面重新创建三个子组件 -> 321123
-> 删除、销毁后面三个子组件 -> 321
。
发现问题了吧?这是毁灭性的灾难,创建新的组件和销毁组件的成本你们晓得的伐……本来仅仅是对组件移动位置就可以完成的更新,被我们毁成这样了。
经过这样的一段旅行,diff
这个庞大的过程就结束了。
我们收获了什么?
用组件唯一的 id
(一般由后端返回)作为它的 key
,实在没有的情况下,可以在获取到列表的时候通过某种规则为它们创建一个 key
,并保证这个 key
在组件整个生命周期中都保持稳定。
别用 index
作为 key
,和没写基本上没区别。
千万别用随机数作为 key
,不然你的老板会被你气死。
transition组件会向下查找第一个真实vnode
,也就是说keep-alive
这种会被排除,并且在vnode.data.transition中保存了动画相关的值。
然后在src/platforms/web/modules/transition.js
这个模块中,定义了动画vnode在enter和destory的时机应该如何添加动画,比如在合适的时机加上动画类名。
const autoCssTransition: (name: string) => Object = cached(name => {
return {
enterClass: `${name}-enter`,
enterToClass: `${name}-enter-to`,
enterActiveClass: `${name}-enter-active`,
leaveClass: `${name}-leave`,
leaveToClass: `${name}-leave-to`,
leaveActiveClass: `${name}-leave-active`
}
})
主要的进入动画链路:
if (expectsCSS) {
addTransitionClass(el, startClass)
addTransitionClass(el, activeClass)
nextFrame(() => {
removeTransitionClass(el, startClass)
if (!cb.cancelled) {
addTransitionClass(el, toClass)
if (!userWantsControl) {
if (isValidDuration(explicitEnterDuration)) {
setTimeout(cb, explicitEnterDuration)
} else {
whenTransitionEnds(el, type, cb)
}
}
}
})
}
注意是在nextFrame才加上了toClass,否则会被直接合并到一次渲染中去,这涉及到浏览器的eventLoop。
这里是双层的raf,只有这样才能保证下一帧调用。
const raf = inBrowser
? window.requestAnimationFrame
? window.requestAnimationFrame.bind(window)
: setTimeout
: /* istanbul ignore next */ fn => fn()
export function nextFrame (fn: Function) {
raf(() => {
raf(fn)
})
}
细节1:加上appear参数,可以使得组件在初次挂载时也能有动画。
细节2:抽象组件不会出现在组件的父子关系链中
细节3: whenTransitionEnds对于一个dom节点如何判断动画结束时机,如果transition-duration
的值有多个,说明监听了多个动画属性,那么要利用类似于Promise.all
的思路去监听全部的动画执行完毕。
// whenTransitionEnds
export function whenTransitionEnds (
el: Element,
expectedType: ?string,
cb: Function
) {
const { type, timeout, propCount } = getTransitionInfo(el, expectedType)
if (!type) return cb()
const event: string = type === TRANSITION ? transitionEndEvent : animationEndEvent
let ended = 0
const end = () => {
el.removeEventListener(event, onEnd)
cb()
}
const onEnd = e => {
if (e.target === el) {
if (++ended >= propCount) {
end()
}
}
}
setTimeout(() => {
if (ended < propCount) {
end()
}
}, timeout + 1)
el.addEventListener(event, onEnd)
}
function isUrl(url) {
try {
new Url(url)
return true
} catch (error) {
return false
}
}
引入了额外的学习成本,但是可以避免eject出cra的webpack配置。
一步步拆解 compose
函数,看看它到底做了什么样的事情,比较脑壳痛。
假设现在是三个高阶组件的组合:
const compsosed = compose(
withA,
withB,
withC,
withD
)
const hoc = compsosed(view)
reduce
的第一次循环里,a
是 withA
,b
是 withB
,然后 return 了:(...args) => withA(withB(...args))
这个 return 的值就会作为 reduce
中下次循环的 a
b
是我们假设的另一个高阶组件 withC
,那么就 return 了(...args2) => (...args) => withA(withB(...args))(withC(...args2))
↑ 这里是a ↑这里是(b(args))
b
是我们假设的另一个高阶组件 withD
,那么就 return 了(...args3) => (...args2) => (...args) => (withA(withB(...args))(withC(...args2)))(withD(...args3))
view
,上一步中的 args2
就会被消除,这个函数会先归约成这样:(...args2) => (...args) => (withA(withB(...args))(withC(...args2)))(withD(view))
withD(view)
又会作为...args2
进一步执行,归约为:
(...args) => (withA(withB(...args))(withC(withD(view))))
withC(withD(view))
又会进一步的作为 ...args
归约。
withA(withB(withC(withD(view)))
可以看到,compose
函数不断的把函数高阶包裹,在执行的时候又一层一层的解包,非常巧妙的构思。
不知道能不能很好的嵌入进其他的文档里,毕竟这个只会根据类型定义生成文档,再调研下。
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.