Code Monkey home page Code Monkey logo

blob's Introduction

blob's People

Contributors

dravenww avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

blob's Issues

从JavaScript的运行机制来了解Vue的nextTick

前篇

nextTick(flushSchedulerQueue)

nextTick是Vue里面一个比较核心的概念;不过在讲nextTick之前就必须要讲到JavaScript的运行机制和任务队列。

JavaScript的运行机制

众所周知,浏览器的脚本语言是JavaScript,这个语言最大的特点就是单线程,也就是在同一时间只能干一件事情。

为什么是单线程的呢?假定有两个线程,一个操作dom,一个删除dom,岂不就乱套了~

当然为了充分利用CPU,Html5提出了web worker,允许开发人员创建多个线程,但是子线程完全受主线程控制,但是不得操作DOM,这也是遵循了单线程的标准。

单线程呢,也就意味着所有的任务,都需要排队运行,一个任务运行结束后,才会去执行下一个任务;

熟悉JavaScript的开发人员都明白,有异步回调这个概念,也就是说会挂起等待中的任务,去执行下一个任务,等回调回来再去执行被挂起的任务。

综上所述,任务分为两种,一个是同步任务(synchronous,简称sync),一个是异步任务(asynchronous,简称async)。

  • 同步任务指的是,在主线程上面排队执行的任务,一个任务的结束,才能执行下一个任务;
  • 异步任务指的是,不在主线程上面的任务,而是在任务队列中,主线程执行完成后询问任务队列,从任务队列中取的一个任务,放到主线程中执行。
    所以简要图示一下,就是这样的:

主进程会不断重复获取步骤,执行完一个qtask,则继续询问qtask任务队列,获取qtask,放到主线程来执行。

只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制

任务队列

任务队列,也就是异步任务的队列。分为两种类型的任务:微任务(microtask)和宏任务(macrotask)

宏任务(macrotask):

  • 包括:setTimeout、setInterval、setImmediate、I/O、UI renderingmacrotask事件;
  • 可以理解为浏览器执行完当前宏任务后,在下一个宏任务执行之前,浏览器就会开始进行渲染;
  • 宏任务一般是当前事件循环的最后一个任务,浏览器的ui绘制会插在每个宏任务之间,阻塞宏任务会导致浏览器ui不渲染;
  • 其实也可以把主线程的任务当作第一个宏任务来看待。

微任务(microtask):

  • 包括:Promises(浏览器实现的原生Promise)、MutationObserver、process.nextTick;
  • 浏览器进行ui渲染之前执行的任务,也就是ui渲染是在微任务执行完成后才开始的;
  • 值得注意的是,过多的微任务会阻塞浏览器的渲染;给microtask队列添加过多回调阻塞macrotask队列的任务;
  • 鉴于上面问题,浏览器考虑性能的问题,也会对微任务的数量进行限制;
  • 事件的冒泡行为,也是在微任务后执行,微任务的优先级是最高的;
    举个例子:
console.log('main start');

setTimeout(() => {
  console.log('macrotask');
  Promise.resolve().then(() => {
    console.log('microtask 1');
  })
}, 0);

Promise.resolve().then(() => {
  console.log('microtask 2');
  Promise.resolve().then(() => {
    console.log('microtask 3');
  })
})

console.log('main end');

上面模仿了一下微任务(Promise)和宏任务(setTimeout);微任务里面套了个微任务;宏任务里面套了个微任务;
输出如下:

main start
main end
microtask 2
microtask 3
macrotask
microtask 1

可以分析下上面代码的执行顺序:

  • 第一步:先执行的是主线程的代码main start和main end;
  • 第二步:开始执行微任务microtask2,执行microtask2过程中,又添加了一个microtask3的微任务,
  • 第三步:执行完microtask2后,继续从microtask队列中取微任务,发现有刚在执行microtask2过程中放进去的3,取出microtask3来执行microtask3;
  • 第四步:执行完microtask3后,继续从microtask队列中去取微任务,此时微任务队列为空,则去宏任务队列中取任务,取到macrotask;
  • 第五步:执行macrotask,执行的过程中,又往微任务队列存了个microtask1;
  • 第六步:执行完macrotask后,此时一个宏任务执行完成,开始下一轮重复,也就回到了上面的步骤2,微任务队列获取微任务,发现了microtask1;
  • 第七步:执行microtask1,执行完成,程序运行完成。

综上分析任务队列完成

nextTick

经过上面的过程,相信大家都对浏览器的运行机制和任务队列有了足够的了解,也明白了任务队列中任务的执行顺序,接下来咱们看下nextTick的实现。文件位于/src/core/util/next-tick.js,先看下Vue里面任务的代码:

let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    console.log('counter', counter)
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

上面代码是Vue对timerFun的定义,Vue倾向于微任务,毕竟微任务优先级是最高的,咱们来看下实现:

  • 最优先采用的是Promise,直接使用的也是咱们上面的例子:Promise.resolve().then(flushCallbacks);
  • 如果浏览器不支持原生的Promise,退而求其次,使用浏览器自带的MutationObserver;MutationObserver,它会在指定的DOM发生变化时被调用;Vue的实现方式是创建一个dom节点,通过改变节点的内容,来触发MutationObserver的回调:new MutationObserver(flushCallbacks);
  • 如果浏览器也不支持MutationObserver,那没办法了,只能使用宏任务了setImmediate和setTimeout,这两个在Vue里面使用方式是一样的,两者的执行顺序在无I/O的时候说不准,不过在有I/O的时候setImmediate是会先被执行的,这可能也是Vue先考虑使用setImmediate的原因吧。

来看下nextTick的代码吧:

function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      console.log('_resolve')
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

上面部分代码,会对传进来的cb进行存储,放到全局变量callbacks里面,然后判断当前执行的状态,是否属于pending(类似Promise的pending状态)状态,可以理解为忙着呢,如果不忙,就让它忙起来,执行上面部分讲到的timerFun;这部分也就是咱们经常用到的

$nextTick(function() {
	dosomething....
})

下面咱们来看下调用timerFun后,执行的flushCallbacks;

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

执行开始,置为不忙状态,因为浏览器是单线程的,执行这段代码的时候,就不会执行别的代码,不用担心这时候会有别的事情影响此处代码的执行,也不用担心此时会有nextTick的调用,也就不用担心pending状态的此处改变会不会影响nextTick部分的逻辑;

此处代码很简单,获取回调的拷贝,然后把回调栈清空;依次执行回调。

#记得star

从源码分析Vue3的dom diff

本篇文章将会着重讲解Vue3中数据发生更新时所做的事情。

例子代码

本篇将要讲解dom diff,那么咱们结合下面的例子来进行讲解,这个例子是在上一篇文章的基础上,加了一个数据变更,也就是list的值发生了改变。html中增加了一个按钮change,通过点击change按钮来调用change函数,来改变list的值。例子位于源代码/packages/vue/examples/classic/目录下,下面是例子的代码:

const app = Vue.createApp({
    data() {
        return {
            list: ['a', 'b', 'c', 'd']
        }
    },
    methods: {
        change() {
            this.list = ['a', 'd', 'e', 'b']
        }
    }
});
app.mount('#demo')
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport"
          content="initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0,user-scalable=no,target-densitydpi=medium-dpi,viewport-fit=cover"
    />
    <title>Vue3.js hello example</title>

    <script src="../../dist/vue.global.js"></script>
</head>
<body>
<div id="demo">
    <ul>
        <li v-for="item in list" :key="item">
            {{item}}
        </li>
    </ul>
    <button @click="change">change</button>
</div>
<script src="./hello.js"></script>
</body>
</html>

源码解读

关于Vue3中数据发生变更,最终影响到页面发生变化的过程,我们本篇文章只对componentEffect以及以后的代码进行讲解,对于数据变更后,是如何执行到componentEffect函数,以及为何会执行componentEffect,后面的文章再进行讲解。

componentEffect

来看下componentEffect更新部分的代码:

  // @file packages/runtime-core/src/renderer.ts
  function componentEffect() {
    if (!instance.isMounted) {
    	// first render
    } else {
        let {next, bu, u, parent, vnode} = instance
        let originNext = next
        let vnodeHook: VNodeHook | null | undefined

        if (next) {
            updateComponentPreRender(instance, next, optimized)
        } else {
            next = vnode
        }
        next.el = vnode.el

        // beforeUpdate hook
        if (bu) {
            invokeArrayFns(bu)
        }
        // onVnodeBeforeUpdate
        if ((vnodeHook = next.props && next.props.onVnodeBeforeUpdate)) {
            invokeVNodeHook(vnodeHook, parent, next, vnode)
        }
        const nextTree = renderComponentRoot(instance)
        const prevTree = instance.subTree
        instance.subTree = nextTree

        if (instance.refs !== EMPTY_OBJ) {
            instance.refs = {}
        }
        patch(
            prevTree,
            nextTree,
            hostParentNode(prevTree.el!)!,
            getNextHostNode(prevTree),
            instance,
            parentSuspense,
            isSVG
        )
        next.el = nextTree.el
        if (originNext === null) {
            updateHOCHostEl(instance, nextTree.el)
        }
        // updated hook
        if (u) {
            queuePostRenderEffect(u, parentSuspense)
        }
        // onVnodeUpdated
        if ((vnodeHook = next.props && next.props.onVnodeUpdated)) {
            queuePostRenderEffect(() => {
                invokeVNodeHook(vnodeHook!, parent, next!, vnode)
            }, parentSuspense)
        }
    }
  }

当数据发生变化的时候,最终会走到上面的else的逻辑部分。

  • 默认情况下next是null,父组件调用processComponent触发当前调用的时候会是VNode,此时next为null;
  • 调用当前实例beforeUpdate钩子函数;调用要更新的Vnode(next)的父组件的beforeUpdate钩子函数;
  • 获取当前实例的vNode => prevTree;获取要更新的vNode=> nextTree;然后调用patch;

调用patch函数的过程,也就是根据VNode的type,走不同的支流的过程;点击change按钮:n1的值:
n2的值:
根据这个值,可以知晓,会走到processFragment函数;

processFragment

调用processFragment(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)函数,参数的值:

  • 此时n1和n2如上图;
  • container为#demo;
  • anchor为null;
  • parentComponent为instance实例;
  • parentSuspense为null;
  • isSVG为false;
  • optimized为false;

来看下processFragment函数的源码:

// @file packages/runtime-core/src/renderer.ts
const processFragment = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
) => {
    const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(''))!
    const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(''))!

    let {patchFlag, dynamicChildren} = n2
    if (patchFlag > 0) {
        optimized = true
    }

    if (n1 == null) {
    	// first render的逻辑
    } else {
        if (
            patchFlag > 0 && patchFlag & PatchFlags.STABLE_FRAGMENT && dynamicChildren
        ) {
            patchBlockChildren(
                n1.dynamicChildren!,
                dynamicChildren,
                container,
                parentComponent,
                parentSuspense,
                isSVG
            )
            if (__DEV__ && parentComponent && parentComponent.type.__hmrId) {
                traverseStaticChildren(n1, n2)
            } else if (
                n2.key != null ||
                (parentComponent && n2 === parentComponent.subTree)
            ) {
                traverseStaticChildren(n1, n2, true /* shallow */)
            }
        } else {
            patchChildren(
                n1,
                n2,
                container,
                fragmentEndAnchor,
                parentComponent,
                parentSuspense,
                isSVG,
                optimized
            )
        }
    }
}

刨除掉first render的代码后,可以看到下面还是分为了两个分支;根据n1和n2可知,我们将会走if分支,执行patchBlockChildren。

patchBlockChildren

调用patchBlockChildren(n1.dynamicChildren, n2.dynamicChildren, container, parentComponent, parentSuspense, isSVG)函数,此时参数如下:

  • oldChildren:n1.dynamicChildren,也就是Symbol(Fragment) =>ul 和button两个元素组成的数组;
  • newChildren: n2.dynamicChildren,也就是Symbol(Fragment) =>ul 和button两个元素组成的数组;
  • fallbackContainer:container,也就是#demo;
  • parentComponent:instance实例;
  • parentSuspense:null;
  • isSVG:false。

来看下patchBlockChildren的源码:

// @file packages/runtime-core/src/renderer.ts
const patchBlockChildren: PatchBlockChildrenFn = (
    oldChildren,
    newChildren,
    fallbackContainer,
    parentComponent,
    parentSuspense,
    isSVG
) => {
    for (let i = 0; i < newChildren.length; i++) {
        const oldVNode = oldChildren[i]
        const newVNode = newChildren[i]
        const container =
            oldVNode.type === Fragment ||
            !isSameVNodeType(oldVNode, newVNode) ||
            oldVNode.shapeFlag & ShapeFlags.COMPONENT ||
            oldVNode.shapeFlag & ShapeFlags.TELEPORT
                ? hostParentNode(oldVNode.el!)!
                : fallbackContainer
        patch(
            oldVNode,
            newVNode,
            container,
            null,
            parentComponent,
            parentSuspense,
            isSVG,
            true
        )
    }
}

可以看到patchBlockChildren是for循环调用patch函数,上面看到newChildren是一个长度为2的数组。循环遍历调用patch(oldVNode, newVNode, container, null, parentComponent, parentSuspense, isSVG, true);

  • 第一次循环:
    • oldVNode:老的ul数组生成的VNode对象;
    • newVNode:新的ul数组生成的VNode对象;
    • container:ul元素;
    • anchor:上面传递的是null;
    • parentComponent: instance实例;
    • parentSuspense: null;
    • isSVG: false;
    • optimized: true;
  • 第二次循环:
    • oldVNode: 老的change按钮构成的VNode对象;
    • newVNode:新的change按钮构成的VNode对象;
    • container:此时的container为#demo;
    • anchor:上面传递的是null;
    • parentComponent: instance实例;
    • parentSuspense: null;
    • isSVG: false;
    • optimized: true;

processElement

咱们先说第二次循环,第二次比较简单;上面说到调用patch函数,通过上面了解到第二次循环newVNode的type是button;则会走到processElement,参数全部是透传过来的:

const processElement = (
        n1: VNode | null,
        n2: VNode,
        container: RendererElement,
        anchor: RendererNode | null,
        parentComponent: ComponentInternalInstance | null,
        parentSuspense: SuspenseBoundary | null,
        isSVG: boolean,
        optimized: boolean
    ) => {
        isSVG = isSVG || (n2.type as string) === 'svg'
        if (n1 == null) {
            // first render
        } else {
            patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
        }
    }

如上代码,会直接调用patchElement,此时参数为:

  • n1: 老的change按钮构成的VNode对象;
  • n2:新的change按钮构成的VNode对象;
  • parentComponent: instance实例;
  • parentSuspense: null;
  • isSVG: false;
  • optimized: true;

patchChildren

现在再来说第一次循环,执行patch的时候,newVNode的type为Symbol(Fragment) => ul,此时还是会走到processFragment函数,不过此时的dynamicChildren为空,会继续运行到patchChildren函数。

patchChildren

此时运行到patchChildren函数,我们来看下运行到此时的参数:

  • n1:老的ul数组生成的VNode对象;
  • n2:新的ul数组生成的VNode对象;
  • container:ul元素;
  • anchor:ul结尾生成的对象;
  • parentComponent:instance实例;
  • parentSuspense:null
  • isSVG:false;
  • optimized:true;

下面看下patchChildren的源码:

const patchChildren: PatchChildrenFn = (
    n1,
    n2,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized = false
) => {
    const c1 = n1 && n1.children
    const prevShapeFlag = n1 ? n1.shapeFlag : 0
    const c2 = n2.children

    const {patchFlag, shapeFlag} = n2
    if (patchFlag > 0) {
        if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
            patchKeyedChildren(
                c1 as VNode[],
                c2 as VNodeArrayChildren,
                container,
                anchor,
                parentComponent,
                parentSuspense,
                isSVG,
                optimized
            )
            return
        } else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
            // patchUnkeyedChildren
            return
        }
    }

    // other ......
}

此时patchFlag的值为128,同时我们的list渲染是有key的,so 会运行patchKeyedChildren函数,c1为四个li组成的数组(a,b,c,d);c2为新的li组成的数组(a,d,e,b);其他值透传到patchKeyedChildren。

patchKeyedChildren

上面对patchKeyedChildren函数的参数已经进行了说明,在这里我们再回顾下:

  • c1:四个li组成的数组(a,b,c,d);
  • c2:新的li组成的数组(a,d,e,b);
  • container:ul元素;
  • parentAnchor:ul结尾生成的对象;
  • parentComponent:instance实例;
  • parentSuspense:null
  • isSVG:false;
  • optimized:true;

接下来看下patchKeyedChildren函数的源码:

const patchKeyedChildren = (
    c1: VNode[],
    c2: VNodeArrayChildren,
    container: RendererElement,
    parentAnchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
) => {
    let i = 0
    const l2 = c2.length
    let e1 = c1.length - 1 
    let e2 = l2 - 1 

    while (i <= e1 && i <= e2) {
        const n1 = c1[i]
        const n2 = (c2[i] = optimized ? cloneIfMounted(c2[i] as VNode) : normalizeVNode(c2[i]))
        if (isSameVNodeType(n1, n2)) {
       		patch(n1,n2,container,null,parentComponent,parentSuspense,isSVG,optimized)
        } else {
            break
        }
        i++
    }

    while (i <= e1 && i <= e2) {
        const n1 = c1[e1]
        const n2 = (c2[e2] = optimized ? cloneIfMounted(c2[e2] as VNode) : normalizeVNode(c2[e2]))
        if (isSameVNodeType(n1, n2)) {
            patch(n1,n2,container,null,parentComponent,parentSuspense,isSVG,optimized)
        } else {
            break
        }
        e1--
        e2--
    }

    if (i > e1) {
        if (i <= e2) {
            const nextPos = e2 + 1
            const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
            while (i <= e2) {
                patch(
                    null,
                    (c2[i] = optimized
                        ? cloneIfMounted(c2[i] as VNode)
                        : normalizeVNode(c2[i])),
                    container,
                    anchor,
                    parentComponent,
                    parentSuspense,
                    isSVG
                )
                i++
            }
        }
    }

    else if (i > e2) {
        while (i <= e1) {
            unmount(c1[i], parentComponent, parentSuspense, true)
            i++
        }
    }

    else {
        const s1 = i
        const s2 = i 
        for (i = s2; i <= e2; i++) {
            const nextChild = (c2[i] = optimized ? cloneIfMounted(c2[i] as VNode) : normalizeVNode(c2[i]))
            if (nextChild.key != null) {
                keyToNewIndexMap.set(nextChild.key, i)
            }
        }

        let j
        let patched = 0
        const toBePatched = e2 - s2 + 1
        let moved = false
        let maxNewIndexSoFar = 0
        const newIndexToOldIndexMap = new Array(toBePatched)
        for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0

        for (i = s1; i <= e1; i++) {
            const prevChild = c1[i]
            if (patched >= toBePatched) {
                unmount(prevChild, parentComponent, parentSuspense, true)
                continue
            }
            let newIndex
            if (prevChild.key != null) {
                newIndex = keyToNewIndexMap.get(prevChild.key)
            } else {
                for (j = s2; j <= e2; j++) {
                    if (
                        newIndexToOldIndexMap[j - s2] === 0 &&
                        isSameVNodeType(prevChild, c2[j] as VNode)
                    ) {
                        newIndex = j
                        break
                    }
                }
            }
            if (newIndex === undefined) {
                unmount(prevChild, parentComponent, parentSuspense, true)
            } else {
                newIndexToOldIndexMap[newIndex - s2] = i + 1
                if (newIndex >= maxNewIndexSoFar) {
                    maxNewIndexSoFar = newIndex
                } else {
                    moved = true
                }
                patch(
                    prevChild,
                    c2[newIndex] as VNode,
                    container,
                    null,
                    parentComponent,
                    parentSuspense,
                    isSVG,
                    optimized
                )
                patched++
            }
        }

        const increasingNewIndexSequence = moved
            ? getSequence(newIndexToOldIndexMap)
            : EMPTY_ARR
        j = increasingNewIndexSequence.length - 1
        for (i = toBePatched - 1; i >= 0; i--) {
            const nextIndex = s2 + i
            const nextChild = c2[nextIndex] as VNode
            const anchor =
                nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor
            if (newIndexToOldIndexMap[i] === 0) {
                patch(
                    null,
                    nextChild,
                    container,
                    anchor,
                    parentComponent,
                    parentSuspense,
                    isSVG
                )
            } else if (moved) {
                if (j < 0 || i !== increasingNewIndexSequence[j]) {
                    move(nextChild, container, anchor, MoveType.REORDER)
                } else {
                    j--
                }
            }
        }
    }
}

上面代码包含的有两个while循环和两对if-else;

  • i=0,循环开始下标;e1、e2为c1和c2的长度;l2为新的children的长度;
  • 第一个while循环,从头开始对列表进行遍历:
    • 当nodeType一样的时候,调用patch;
    • 当nodeType不一样的时候,跳出循环;
  • 第二个while循环,当第一个while循环对c1和c2都没有遍历完的时候,从尾部开始对其进行遍历:
    • 当nodeType一样的时候,调用patch;
    • 当nodeType不一样的时候,跳出循环;
  • 第一个if,i>e1证明c1已经遍历完,i<=e2证明c2还没遍历完,对剩余的c2继续遍历,调用patch;
  • 第二个else-if,i>e2证明c2已经遍历完,i<=e1证明c1还没遍历完,对剩余的c1继续遍历,因为c1为老的列表,则调用unmount把无用的列表内容卸载掉:
  • 第二个else:c1和c2至少有一个没有遍历完,走到最后一个else的逻辑:
    • for (i = s2; i <= e2; i++)for循环遍历剩余c2,收集每个c2的元素的key,构成map => keyToNewIndexMap;
    • for (i = 0; i < toBePatched; i++)for循环遍历剩余c2部分长度来生成映射,并赋值为0;
    • for (i = s1; i <= e1; i++) for循环遍历剩余c1,使用key进行直接获取(for循环剩余c2进行获取)newIndex,此处证明还是要绑定好key,唯一性很重要;newIndex有值说明c2中存在当前老的元素在c1中,老的preChild,在c2中还需要,则调用patch;如果newIndex为undefined,则说明老的preChild在c2中不需要了,调用unmount,把当前preChild卸载掉;
    • 遍历完剩余c1后,再倒着遍历剩余c2:for (i = toBePatched - 1; i >= 0; i--);如果(newIndexToOldIndexMap[i] === 0则证明当前nextChild为新的节点,调用patch;否则判断之前是否发生了移动moved,经过逻辑判断,调用move;

patchKeyedChildren 例子

根据咱们上面的例子,由old: ['a', 'b', 'c', 'd']变更为new: ['a', 'd', 'e', 'b']的过程如下:

  • 首先进入第一个while循环,此时i为0,l2为4,e1为3,e2为3;
    • 第一次循环,old-a与new-a是相同的,调用patch,不发生变化;
    • 第二次循环,old-b与new-b是不相同的,break;
    • 跳出循环,从头开始的循环结束;
  • 进入第二个while循环,此时i为1,l2为4,e1为3,e2为3;
    • 第一次循环,old-d与new-b是不相同的,break;
    • 跳出循环,从尾部开始的循环结束;
  • 进入第一个if判断为false,进入第二个else-if判断为false,进入else;
  • for循环收集每个c2的元素的key,keyToNewIndexMap = ['d' => 1, 'e' => 2, 'b' => 3];
  • 建立长度为剩余c2长度的数组newIndexToOldIndexMap = [0, 0 ,0];
  • 此时进入for (i = s1; i <= e1; i++) for循环遍历剩余c1阶段,此时i为1,s1为1,s2为1:
    • 第一次循环:遍历的元素为old-b,发现在new中存在,通过keyToNewIndexMap获得在new中的index为3;调用patch;
    • 第二次循环:遍历的元素为old-c,在new中不存在,调用unmount卸载当前old-c,改变后c1为['a', 'b', 'd']
    • 第三次循环:遍历的元素为old-d,在new中存在,通过keyToNewIndexMap获得在new中的index为1;调用patch;
    • 跳出循环,遍历c1剩余阶段结束;
  • 此时进入for (i = toBePatched - 1; i >= 0; i--)倒着遍历剩余c2阶段,此时i为2,j为0,s1为1,s2为1,newIndexToOldIndexMap为[4, 0, 2]:
    • 第一次循环,判断当前nextChild(new-b)存不存在,通过newIndexToOldIndexMap发现nextChild存在,并且在old里面的索引值为2,j--,此时j为-1;i--,i为1;
    • 第二次循环,判断当前nextChild(new-e)存不存在,通过newIndexToOldIndexMap发现nextChild的索引值为0,表示不存在,则调用patch;i--,i为0;改变后c1为['a', 'e', 'b', 'd'];
    • 第三次循环,判断当前nextChild(new-d)存不存在,通过newIndexToOldIndexMap发现nextChild的索引值为4,表示存在,则调用move;i--,i为-1;改变后c1为['a',, 'd' 'e', 'b'];
    • 此时i为-1,跳出循环,循环结束
  • 遍历结束,结果变更为了new: ['a', 'd', 'e', 'b']

isSameVNodeType

大家可以看下下面isSameVNodeType的代码,大家在写代码的时候,为了能够提高页面性能,dom diff的速度,如果没有发生变更的元素,key一定要保持一样,不要v-for="(item, index) in list" :key="index"这样来写,因为当只有数组内部元素发生了位置移动而元素未发生改变时,index的值是变更的,这样在dom diff的时候就会使程序发生误解。key的唯一性很重要

export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
    return n1.type === n2.type && n1.key === n2.key
}

结语

本章主要讲解了Vue3在进行更新操作时所做的事情,着重讲解了在dom diff中的操作,也举了个例子方便大家理解,大家可以对比着Vue2的diff来看,Vue3对dom diff进行了优化,比Vue2性能更高。

教你几行代码做个短视频

hello,大家好,我是德莱问。

写作是件锻炼人的事情,也是对自己的一种习惯的培养,对一段时间以来的一个总结,同时也是对这段时间以来所学知识的一个巩固。在越来越卷的前端领域,大家都在疯狂的学习,如果自己偷懒,将会落后于别人。

老天爷是公平的,付出多少辛苦,就有多少回报。有时候可以自己开始欺骗自己,可是真正用到的时候,现实就会啪啪打脸。

(前言)说点事情

当前太平盛世,可是互联网领域可算是一直乱世。今天我们所说的是短视频领域。

短视频已成为一种越来越流行的媒体传播形式。像微视和抖音这种 app,每天都会生产成千上万个精彩短视频。而这些视频也为产品带来了巨大的流量。
随之而来,如何让用户可以快速生产一个短视频;或者产品平台如何利用已有的图片、视频、音乐素材批量合成大量视频就成为一个技术难点。

今天为大家带来的是一个基于node.js的轻量、灵活的短视频制作库。您只需要添加几张图片或视频片段再加一段背景音乐,就可以快速生成一个很酷的的视频短片。

image.png

这篇文章将会带领你从头到尾制作一个短视频。

生成项目并安装依赖

首先得建一个项目,然后执行npm init,一顿回车就好了。

mkdir ffcreator-example && cd ffcreator-example
npm init

接下来进行今天咱们这个包的安装操作

npm install ffcreator
or
yarn add ffcreator

重中之重,ffcreator依赖于FFnpeg,因此必须安装FFmpeg

FFCreatorLite依赖于FFmpeg>=0.9以上版本。请设置FFmpeg为全局变量, 否则需要使用setFFmpegPath添加FFmpeg本机路径。(windows用户的ffmpeg很可能不在您的%PATH中,因此您必须设置%FFMPEG_PATH)。

安装FFmpeg的教程,我只说下windows和mac的哈,关于其他的在上面github里面有更详细的说明,之所以只说下windows和mac,因为对于前端开发人员来说,大多数都是mac,也有部分window。对于其他研发人员,如果想尝试的话,可以进到上面github查看其他环境的安装方式。

windows:

共四分步:下载、解压、设置环境变量、使用。

参考文档

mac:

共四分步:

  • 安装homebrew(如已安装,可忽略,直接进行下一步):

    /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
  • 使用homebrew安装ffmpeg:

    brew install ffmpeg

参考文档

至此,项目、环境、依赖都ready了,我们可以进行下一步的操作了。

关于使用

ffcreator是一个node的库,提供了多种构造函数可以进行使用:

  • FFScene, // 屏幕,也称场景

    // 新建一个显示屏
    const scene = new FFScene();
    // 设置背景色   
    scene.setBgColor('#30336b');    
    // 设置停留时长   
    scene.setDuration(8.5);             
    // 设置过渡动画(类型, 时间)
    scene.setTransition('Fat', 1.5);    
    // 把屏幕添加到视频创造器实例上面
    creator.addChild(scene);
  • FFNode, 下面所有类型的基类,可以直接看下面。

  • FFText, 文本元素

    const text = new FFText({text: '这是一个文字', x: 250, y: 80});
    // 文字颜色
    text.setColor('#ffffff');         
    // 背景色
    text.setBackgroundColor('#b33771');         
    // 出现动画为fadeIn,动画的时长1秒,delay时间为1秒,
    text.addEffect("fadeIn", 1, 1);   
    // 设置文本水平居中
    text.alignCenter();                         
    // 设置样式object 
    text.setStyle({padding: [4, 12, 6, 12]});   
    // 把当前文本节点添加到屏幕上面
    scene.addChild(text);
  • FFImage, 图片元素

    // 创建一个图片元素,图片路径为../images/demo.png
    const img = new FFImage({path: '../images/demo.png'});
    // 设置位置   
    img.setXY(250, 340);                
    // 设置缩放
    img.setScale(2);                   
    // 设置旋转   
    img.setRotate(45);               
    // 设置透明度 
    img.setOpacity(0.3);                
    // 设置宽高
    img.setWH(100, 200);                
    // 设置动画效果
    // 设置动画效果为slideInDown,动画时长为1.2秒,delay时间为0
    img.addEffect("slideInDown", 1.2, 0);
    // 把当前图片节点添加到屏幕上面
    scene.addChild(img);
  • FFVideo, 视频元素

    // 创建一个视频元素,视频路径为../videos/demo.mp4,位置在屏幕的100和150处
    // 宽度为500,高度为350.
    const video = new FFVideo({
        path: videopath,
        x: 100,
        y: 150,
        width: 500,
        height: 350
    });
    设置是否有音乐
    video.setAudio(true);  
    // 设置是否循环播放
    video.setLoop(true);
    // 截取播放时长,设置视频播放的开始时间和结束时间
    video.setTimes("00:00:43", "00:00:50");
    // 单独设置视频播放的开始时间
    video.setStartTime("00:00:43"),
    // 单独设置视频播放的结束时间
    video.setEndTime("00:00:50"),
    // video还有很多其他的方法
    ...
    // 把当前视频元素添加到屏幕上面
    scene.addChild(video);
  • FFAlbum, 相册元素

    // 新建相册元素。
    const album = new FFAlbum({
        list: [img1, img2, img3, img4],   // 相册的图片集合
        x: 250,
        y: 300,
        width: 500,
        height: 300,
    });
     // 设置相册切换动画
    album.setTransition('zoomIn');     
    // 设置单张停留时长
    album.setDuration(2.5);             
    // 设置单张动画时长
    album.setTransTime(1.5);            
    scene.addChild(album);
    // 把当前相册元素添加到屏幕上面
    scene.addChild(video);
  • FFVtuber, 虚拟主播形象

    const vtuber = new FFVtuber({ x: 100, y: 400 });
    // 设置动画时间循环周期
    vtuber.setPeriod([
        [0, 3],
        [0, 3]
    ]);
    // 路径设置这里 baby/[d].png 和 baby/%d.png 两种方式均可以
    const vpath = path.join(__dirname, "./avator/baby/[d].png");
     // 从第1-7.png
    vtuber.setPath(vpath, 1, 7);   
    // 播放速度
    vtuber.setSpeed(6);             
    creator.addVtuber(vtuber);
  • FFSubtitle, 字幕元素

    const content = '跟计算机工作酷就酷在这里,它们特别听话,说让干什么就干什么...';
    const subtitle = new FFSubtitle({
        text: content,
        comma: true,                  // 是否逗号分割
        backgroundColor: '#00219C',   // 背景色
        color: '#fff',                // 文字颜色
        fontSize: 24                  // 字号
    });
    // 设置文案,也可以放到conf里
    subtitle.setText(content);      
    // 缓存帧
    subtitle.frameBuffer = 24;      
    // 设置字幕总时长
    subtitle.setDuration(12);       
    scene.addChild(subtitle);
    // 设置语音配音-tts
    subtitle.setSpeech(dub);        
  • FFTween, 渐变

除了上面几种类型之外,还有实例和运行:

  • FFCreator,// 创建一个实例

    const creator = new FFCreator({
      cacheDir,                 // 缓存目录
      outputDir,                // 输出目录
      output,                   // 输出文件名(FFCreatorCenter中可以不设)
      width: 500,               // 影片宽
      height: 680,              // 影片高
      audioLoop: true,          // 音乐循环
      fps: 24,                  // fps
      threads: 4,               // 多线程(伪造)并行渲染
      debug: false,             // 开启测试模式
      defaultOutputOptions: null,// ffmpeg输出选项配置
    });
    
    // 往创造器实例里面添加屏幕
    creator.addChild(scene);
    // 创造器的开始函数。启动。
    creator.start();
  • FFCreatorCenter, // 核心运行库,通过addTask的方式去运行

// 可以通过这种方式启动多个任务,
FFCreatorCenter.addTask(createFFTask)
当然也可以不使用FFCreatorCenter,直接运行
createFFTask();

有demo的哦

  • 图片动画:

图片动画demo地址, demo源码地址

  • 多图相册:

多图相册demo地址, demo源码地址

  • 场景过渡:

场景过渡demo地址, demo源码地址

  • 配音字幕:

配音字幕demo地址, demo源码地址

  • 视频动画:

视频动画demo地址, demo源码地址

写在最后

短视频横行互联网,何不顺应潮流,用代码去实现短视频的创作呢?

既然可以node实现短视频的创造,何不搭配服务器,实现拖拽组合,可视化生成短视频呢?

这些应该都是可以实现的。

如果你过得快乐,请努力工作使自己更快乐;如果不过得不快乐,请努力工作让自己变得快乐;总之,工作使我快乐~

祝大家工作顺利,天天快乐哦~

觉得还不错的话,点个star再走哇~

React大型项目状态管理库如何选型?

背景

由于要做一个使用起来比较舒服的轮子,最近研究了下React的状态管理库,当然,仅限在使用层面,也就是用着舒服的角度来选择到底使用哪个状态管理库。本着在Github上面看看React社区内状态管理库的流行程度和使用程度的层面,来进行选型,然后就有了这篇文章,关于我们最后选择了哪个,文章末尾告知。

选择库的原则如下:

  • 全面拥抱typescript,so选择的库需要对typescript支持友好
  • react自从16.8引入hooks,接下来就开始了函数式编程的时代,所以不要有class这玩意
  • 一定要使用简单,不要太复杂,使用起来很轻量,重点是舒服
  • 支持模块化,可以集中管理,原子性低
  • 支持esmodule,因为后面会考虑迁移到vite,虽然现在还是webpack

截止目前为止,在Github上面看了一下当前比较流行的几个状态管理库的star数和used by的数量,以及npm上面的周下载量(weekly downloads),这可以从某些方面说明明该框架的受欢迎程度,也有很小的可能性不准确,不过很大程度上,对框架选型是有所帮助的。

库名 github star github used npm 周下载量
mobx 23.9k 83.9k 671,787
redux-toolkit 5.9k 83.2k 755,564
recoil 13.5k 83.9k 95,245
zustand 9.4k 7.2k 104,682
rematch 7.3k 2.5k 33,810
concent 950 65 1,263

上面表格中,就是我们接下来要进行挑选的对象,到底中意哪个,还得看看使用起来的时候的姿势,哪个更加舒服。

mobx

mobx是一个非常优秀的react状态管理库,这毋容置疑,而且在Github上面,它的使用量也是做到了第一,官方文档地址zh.mobx.js.org。官网上面给的例子是非常简单的,大多数官网也都如此,可是我不需要简单的例子,我需要一个完整的项目的例子,于是参考了github上面的一个项目antd-pro-mobx。mobx需要搭配mobx-react的连接库一起来使用。

按照npm上面的推荐是要使用class + observe函数包裹的方式,最新版本v6:

import React from "react"
import ReactDOM from "react-dom"
import { makeAutoObservable } from "mobx"
import { observer } from "mobx-react"

// Model the application state.
class Timer {
    secondsPassed = 0

    constructor() {
        makeAutoObservable(this)
    }

    increase() {
        this.secondsPassed += 1
    }

    reset() {
        this.secondsPassed = 0
    }
}

const myTimer = new Timer()

// Build a "user interface" that uses the observable state.
const TimerView = observer(({ timer }) => (
    <button onClick={() => timer.reset()}>Seconds passed: {timer.secondsPassed}</button>
))

ReactDOM.render(<TimerView timer={myTimer} />, document.body)

// Update the 'Seconds passed: X' text every second.
setInterval(() => {
    myTimer.increase()
}, 1000)

新项目的从头开始,应该不会选择老版本的库区使用,一般会选择稳定的新版本的进行使用,关于typescript方面,看源码是已经在使用typescript来编写了,不过在官网和npm上面并没有看到typescript的蛛丝马迹,可能是还没发版吧。

我们对比我们的原则看下关于mobx:

  • 支持typescript -- NO
  • 使用函数式编程 -- NO
  • 使用舒服 -- OK
  • 原子性低问题 -- OK
  • 支持esmodule -- OK

关于mobx部分就暂且到这,说的不对的地方欢迎告知,确实是才疏学浅,没怎么用过这么流行的状态管理库。

reduxjs/toolkit

toolkit,暂且这么叫吧,redux官方状态管理库,cra模板redux(npx create-react-app --template redux)自带状态管理库,cra模板redux-ts(npx create-react-app --template redux-typescript)自带状态管理库.可能这两个模板也导致了toolkit的下载量和使用量非常大。也由于是redux官方库的原因,需要搭配react-redux来搭配使用。这些我们暂且不管,我们看下如何使用,亲测哈,如有使用不当,可以指出。

// index.ts 主入口文件
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { RootStore } from 'modules/store';
import { fetchInfo } from 'modules/counter';

function App(props: any) {
  const count = useSelector((state: RootStore) =>  state.counter.value);
  const dispatch = useDispatch();
  return (
    <div>
      hello home
      <hr/>
        <button
          aria-label="Decrement value"
          onClick={() => dispatch(fetchInfo(2234))}
        >
          fetchInfo
        </button>
        <div>
          <span>{count}</span>
        </div>
    </div>
  );
};

ReactDOM.render(
  <App/>,
  document.getElementById('root'),
);

上面是主入口文件的代码,可以看到这个使用方式还算是比较普遍,符合redux的使用方式。

// modules/store.ts
import { configureStore, combineReducers } from '@reduxjs/toolkit';
import counter from './counter';
import { TypedUseSelectorHook, useSelector, useDispatch } from 'react-redux';

const reducer = combineReducers({
  counter,
});

const store = configureStore({
  reducer,
});

export type RootStore = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootStore> = useSelector;
export default store;

上面是store主文件的代码,这其实也是官方给出的合理使用方式。

// modules/counter.ts
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';

const namespace: string = 'counter';

export interface ICounter {
  value: number;
}

const initialState: ICounter = {
  value: 1,
};

// async 异步函数定义
export const fetchInfo = createAsyncThunk(`${namespace}/fetchInfo`, async (value: number) => {
  await sleep(1000);
  return {
    value: 9000 + value,
  };
});

// 创建带有命名空间的reducer
const counterSlice = createSlice({
  name: namespace,
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchInfo.pending, (state, action) => {
        state.value = 1000;
      })
      .addCase(fetchInfo.fulfilled, (state, {payload}) => {
        state.value = payload.value;
      })
      .addCase(fetchInfo.rejected, (state, {payload}) => {
        state.value = 500;
      });
  },
});

const { reducer } = counterSlice;
export default reducer;

上面是在实际module的使用,会产出一个reducer,唯一不优雅的地方在于extraReducers的调用方式采用了串联的方式,不过还可以通过对象的形式进行传递,不过在ts中支持不够友好,如下:

const counterSlice = createSlice({
  name: namespace,
  initialState,
  reducers: {},
  extraReducers: {
    [fetchInfo.pending.type]: (state: Draft<ICounter>, action: PayloadAction<ICounter>) => {
      state.value = 1000;
    },
    [fetchInfo.pending.type]: (state: Draft<ICounter>, { payload }: PayloadAction<ICounter>) => {
      state.value = payload.value;
    },
    [fetchInfo.pending.type]: (state: Draft<ICounter>, action: PayloadAction<ICounter>) => {
      state.value = 500;
    },
  },
});

可以看到上面换成了对象的方式,不过在函数里面需要自己去写好类型声明;而串行的方式,typescript已经自动推导出了函数所对应的参数类型。

我们对比我们的原则看下关于toolkit:

  • 支持typescript -- OK
  • 使用函数式编程 -- OK
  • 使用舒服 -- OK,除了builder的链式使用方式
  • 原子性低问题 -- OK
  • 支持esmodule -- OK

recoil

recoil,react官方状态管理库,随着react17而来,官方网址为recoiljs.org,其实透过官方文档,我们可以看到差不多是完全遵循了react hooks的使用方式,不需要搭配任何连接器,可以与react直接无缝连接。不过这其实也导致了原子性比较强,统一的状态管理需要对其进行二次封装,而且工作量不小。在typescript方面,0.3.0开始支持,当前为止最新的版本是0.3.1。例子我就看下官方的例子

import React from 'react';
import {
  RecoilRoot,
  atom,
  selector,
  useRecoilState,
  useRecoilValue,
} from 'recoil';

function App() {
  return (
    <RecoilRoot>
      <CharacterCounter />
    </RecoilRoot>
  );
}

const textState = atom({
  key: 'textState', // unique ID (with respect to other atoms/selectors)
  default: '', // default value (aka initial value)
});

function CharacterCounter() {
  return (
    <div>
      <TextInput />
      <CharacterCount />
    </div>
  );
}

function TextInput() {
  const [text, setText] = useRecoilState(textState);

  const onChange = (event) => {
    setText(event.target.value);
  };

  return (
    <div>
      <input type="text" value={text} onChange={onChange} />
      <br />
      Echo: {text}
    </div>
  );
}

const charCountState = selector({
  key: 'charCountState', // unique ID (with respect to other atoms/selectors)
  get: ({get}) => {
    const text = get(textState);

    return text.length;
  },
});

function CharacterCount() {
  const count = useRecoilValue(charCountState);

  return <>Character Count: {count}</>;
}

由上面,我们可以简单再对比下我们的原则:

  • 支持typescript -- OK,虽然力度不是很大
  • 使用函数式编程 -- OK
  • 使用舒服 -- NO
  • 原子性低问题 -- NO
  • 支持esmodule -- OK

zustand

zustand,这个库,说实话,是第一次看到,不过看了npm上面的例子,这个库还是很好用&很实用的,使用起来很舒服,提供的api不是很多,但是够精简,能够满足需求。没有单独的官网,不过readme写的足够详细,算是个地址吧npm zustand, 我们来看下官网提供的例子:

import React from "react";
import create from "zustand";
import PrismCode from "react-prism";
import "prismjs";
import "prismjs/components/prism-jsx.min";
import "prismjs/themes/prism-okaidia.css";

const sleep = (time = 1000) => new Promise((r) => setTimeout(r, time));
const code = `import create from 'zustand'

const useStore = create(set => ({
  count: 1,
  inc: () => set(state => ({ count: state.count + 1 })),
}))

function Controls() {
  const inc = useStore(state => state.inc)
  return <button onClick={inc}>one up</button>
)

function Counter() {
  const count = useStore(state => state.count)
  return <h1>{count}</h1>  
}`;

const useStore = create((set) => ({
  count: 1,
  inc: () => set((state) => ({ count: state.count + 1 })),
  sleep: async () => {
    await sleep(2000);
    set((state) => ({ count: state.count + 30 }));
  }
}));

function Counter() {
  const { count, inc, sleep } = useStore();
  return (
    <div class="counter">
      <span>{count}</span>
      <button onClick={inc}>one up</button>
      <button onClick={sleep}>30 up</button>
    </div>
  );
}

export default function App() {
  return (
    <div class="main">
      <div class="code">
        <div class="code-container">
          <PrismCode className="language-jsx" children={code} />
          <Counter />
        </div>
      </div>
    </div>
  );
}

可以看到所有的数据使用的createStore进行包裹,里面可以定义任意类型,可以是count的这样的stats类型,也可以使用函数(包括异步函数),做到了最简单化;另外zustand还提供了一些其他的工具函数和中间件,关于中间件和工具函数等的如何使用,此处就不多说了,可以去npm看看.

由上面,我们可以简单再对比下我们的原则:

  • 支持typescript -- OK, 但是官网描述里面的例子比较少
  • 使用函数式编程 -- OK
  • 使用舒服 -- OK
  • 原子性低问题 -- 不高不低,中等,可能需要使用到中间件来包裹,扩展使用
  • 支持esmodule -- OK

rematch

rematch, 因为有部分项目在使用这个库,所以简单看了下使用。官网上面有很多例子,可以去看看: rematchjs.org. v1的时候是不支持typescript的,使用上有两种方式(对于effects)

// 方式一
effects: {
  fetchInfo: async () => {
    const res = await requestInfo();
    this.setState({
      ...res;
    })
  }
}
// 方式二
effects: (dispatch) => {
  return {
    fetchInfo: async () => {
      const res = await requestInfo();
      dispatch.info.setInfo(res);
    }
  }
}

v2的时候是增加了typescript的支持,不过却去掉了上面方式一的使用方式,只保留了第二种。具体例子可以前往rematch typescript 查看。这个使用方式其实与上面的redux-toolkit稍微有点相似,不过好像rematch最近下载量下降了不少。

rematch在模块化封装部分做的很好,能够对所有的状态进行统一管理,然后可以按models进行划分功能,使用起来比较舒服。

由上面以及官网上面的一些例子,我们可以简单再对比下我们的原则:

  • 支持typescript -- OK
  • 使用函数式编程 -- OK
  • 使用舒服 -- OK
  • 原子性低问题 -- OK
  • 支持esmodule -- OK

concent

concent,另外一种状态管理器的实现方式,在使用上与Vue3的setup有很大相似之处。为什么会谈到concent,因为有好几个项目在使用concent,而且表现良好。官方网站concentjs.concent功能非常强大,各种黑魔法,要想使用好concent,会有比较大的学习成本,也就是使用起来可能不会很简单。

import { StrictMode } from "react";
import ReactDOM from "react-dom";
import { run, useConcent } from "concent";

const sleep = (time = 1000) => new Promise((r) => setTimeout(r, time));

run({
  counter: {
    state: {
      value: ""
    },
    computed: {},
    lifecycle: {},
    reducer: {
      decrement: async (payload, moduleState) => {
        await sleep(1000);
        return {
          value: moduleState.value - 1
        };
      }
    }
  }
});

const setup = (ctx) => {
  ctx.effect(() => {
    ctx.setState({
      value: 1000
    });
  }, []);
  const increment = () => {
    console.log(1233);
    ctx.setState({
      value: ctx.state.value + 1
    });
  };
  return {
    increment
  };
};

export default function App() {
  const { state, settings, moduleReducer } = useConcent({
    module: "counter",
    setup
  });
  return (
    <div className="App">
      <h1>Hello Counter : {state.value}</h1>
      <div>
        <button onClick={settings.increment}>click increment</button>
        <button onClick={moduleReducer.decrement}>click decrement</button>
      </div>
    </div>
  );
}

ReactDOM.render(
  <StrictMode>
    <App />
  </StrictMode>,
  document.getElementById("root")
);

完整的例子和api可以前往官网查看,支持class装饰器的模式,也支持函数setup的方式,也有不使用setup的方式,都能满足;问题就是api较多,学习成本较高。

根据经验和上面的例子,我们可以简单再对比下我们的原则:

  • 支持typescript -- OK
  • 使用函数式编程 -- OK
  • 使用舒服 -- OK,就是学习成本较高
  • 原子性低问题 -- OK,支持模块化,状态统一管理
  • 支持esmodule -- OK

结论

至此,这几种状态管理库的使用方式和原则对比差不多已经完成。可能有使用不当或者说明错误的地方,欢迎指出。

最终我们考虑了以下几点:

  • 支持typescript,使用函数式编程
  • 支持模块化,状态统一管理
  • 学习成本低,且状态管理库比较流行
  • 考虑大家的意见,这很客观,选择一个大家都愿意使用的库

最终选择的是redux官方的toolkit来进行统一的状态管理。

本文结束,感谢阅读,欢迎讨论交流。如有不当之处感谢指出。

关注

欢迎大家关注我的公众号[德莱问前端],文章首发在公众号上面。

除每日进行社区精选文章收集外,还会不定时分享技术文章干货。

希望可以一起学习,共同进步。

前端面试-计算机基础

计算机基础

http系列

  • 三次握手是什么?为什么需要三次?
  • 四次挥手是什么?为何需要四次?
  • http1、http2、https的区别是什么?
  • https是如何进行加密的?
  • 请求如何取消?AbortController

排序

  • 冒泡排序
// 从小到大排序:
function bubblingSort(list){
     let temp;
     for(let i=0; i<list.length; i++){
          for(let j=i; j<list.length; j++){
               if(list[i] > list[j]){
                    temp = list[i];
                    list[i] = list[j];
                    list[j] = temp;
               }
          }
     }
     return list;
}
let res = bubblingSort([10, 8, 2, 23, 30, 4, 7, 1])
console.log(res); // [1, 2, 4, 7, 8, 10, 23, 30]
  • 直接选择排序
从小到大排序:
function selectSort(list){
     let r,temp;
     for(let j=0; j<list.length; j++){
          for(let i = j+1; i<list.length; i++){
               if(list[j] > list[i]){
                   temp = list[j];
                   list[j] = list[i];
                   list[i] = temp;
               }
          }
     }
     return list;
}
let res = selectSort([10, 8, 2, 23, 30, 4, 7, 1])
console.log(res); // [1, 2, 4, 7, 8, 10, 23, 30]
  • 直接插入排序

整个排序过程为n-1趟插入,即先将序列中第1个记录看成是一个有序子序列,然后从第2个记录开始,逐个进行插入,直至整个序列有序。

function insertSort(list) {
    let flag;
    for(let index = 1; index < list.length; index++) {
        flag = list[index];
        let j = index - 1;
        while (flag < list[j]) {
            list[j + 1] = list[j]
            j--;
        }
        list[j + 1] = flag;
    }
     return list;
}
let res = insertSort([10, 8, 2, 23, 30, 4, 7, 1])
console.log(res); // [1, 2, 4, 7, 8, 10, 23, 30]
  • 希尔排序

排序过程:先取一个正整数d1<n,把所有相隔d1的记录放一组,组内进行直接插入排序;然后取d2<d1,重复上述分组和排序操作;直至di=1,即所有记录放进一个组中排序为止

function shellSort(list) {
    const length = list.length;
    let j, temp;
    for (let d = parseInt(length / 2); d >= 1; d = parseInt(d / 2)) {
        for (let i = d; i < length; i++) {
            temp = list[i];
            j = i - d;
            while (j >= 0 && temp < list[j]) {
                list[j + d] = list[j];
                j -= d;
            }
            list[j + d] = temp;
        }
    }
    return list;
}
let res = shellSort([10, 8, 2, 23, 30, 4, 7, 1])
console.log(res); // [1, 2, 4, 7, 8, 10, 23, 30]

  • 快速排序

通过一次排序,将待排序记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可对这两部分记录进行排序,以达到整个序列有序。

function quickSort(v,left,right){
    if(left < right){
        var key = v[left];
        var low = left;
        var high = right;
        while(low < high){
            while(low < high && v[high] > key){
                high--;
            }
            v[low] = v[high];
            while(low < high && v[low] < key){
                low++;
            }
            v[high] = v[low];
        }
        v[low] = key;
        quickSort(v,left,low-1);
        quickSort(v,low+1,right);
    }
}
let list = [10, 8, 2, 23, 30, 4, 7, 1]
quickSort(list, 0, 7)
console.log(list); // [1, 2, 4, 7, 8, 10, 23, 30]

其他

  • tcp/ip协议的五层模型:应用层、传输层、网络层、数据链路层、物理层
  • 算法相关,leetcode上面刷吧
  • 二叉树等的遍历,前中后序遍历,深度优先,广度优先;
  • 栈、队列的使用
  • 链表的使用

记录一下webpack的chunks

webpack的chunks是通过webpack内部的一个插件来实现的,在3版本及以前使用的是CommonsChunkPlugin;在4版本后开始使用SplitChunksPlugin,我们对于3版本及以前的不做解释,着眼未来,我们来看SplitChunksPlugin;我们根据配置来看:

module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'async',
      minSize: 30000,
      minChunks: 1,
      maxAsyncRequests: 5,
      maxInitialRequests: 3,
      automaticNameDelimiter: '~',
      name: true,
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        }
      }
    }
  }
};

上面是webpack的默认配置,splitChunks就算你什么配置都不做它也是生效的,源于webpack有一个默认配置,这也符合webpack4的开箱即用的特性。

chunks意思为拆分模块的范围,有三个值:async、all和initial;三个值的区别如下:

  • async表示只从异步加载得模块里面进行拆分,也就是动态加载import();
  • initial表示只从入口模块进行拆分;
  • all的话,就是包含上面两者,都会去拆分;
    上面还有几个参数:
  • minChunks,代表拆分之前,当前模块被共享的次数,上面是1,也就是一次及以上次引用,就会拆分;改为2的话,就是两次及以上的引用会被拆分;
  • minSize:生成模块的最小大小,单位是字节;也就是拆分出来的模块不能太小,太小的话进行拆分,多了一次网络请求,因小失大;
  • maxAsyncRequests:用来限制异步模块内部的并行最大请求数的,也就是是每个动态import()它里面的最大并行请求数量,需要注意的是:
    • import()本身算一个;
    • 只算js的,不算其他资源,例如css等;
    • 如果同时又两个模块满足cacheGroup的规则要进行拆分,但是maxInitialRequests的值只能允许再拆分一个模块,那尺寸更大的模块会被拆分出来;
  • maxInitialRequests:表示允许入口并行加载的最大请求数,之所以有这个配置也是为了对拆分数量进行限制,不至于拆分出太多模块导致请求数量过多而得不偿失,需要注意的是
    • 入口本身算一个请求,
    • 如果入口里面有动态加载的不算在内;通过runtimeChunk拆分出的runtime不算在内;只算js的,不算css的;
    • 如果同时又两个模块满足cacheGroup的规则要进行拆分,但是maxInitialRequests的值只能允许再拆分一个模块,那尺寸更大的模块会被拆分出来;
  • automaticNameDelimiter:这是个连接符,不用关注这个;
  • name:该属性主要是为了打包后文件的命名,使用原名字命名为true,按照默认配置,就会是文件名~文件名.**.js
  • cacheGroup:cacheGroups其实是splitChunks里面最核心的配置,splitChunks就是根据cacheGroups的配置去拆分模块的,
    • test:正则匹配路径,表示只筛选从node_modules文件夹下引入的模块,所以所有第三方模块才会被拆分出来。
    • priority:优先级,上面的default的优先级低于vendor;
    • minChunks:这个其实也是个例子,和父层的含义是一样的,不过会覆盖父层定义的值,拆分之前,模块被共享使用的次数;
    • reuseExistingChunk:是否使用以及存在的chunk,字面意思;
      注意:
  • cacheGroups之外设置的约束条件比如说默认配置里面的chunks、minSize、minChunks等等都会作用于cacheGroups,cacheGroups里面的值覆盖外层的配置;
  • test、priority、reuseExistingChunk,这三个是只能定义在cacheGroup这一层的;
  • 如果cacheGroups下面的多个对象的priority相同时,先定义的会先命中;

一文搞懂Vue2和Vue3的Proxy

问个好

hello,大家好,我是德莱问,又和大家见面了。

当在初六抱怨假期为何如此短暂的时候,已然来到了初七。

预祝大家,初七快乐,开工大吉!!!

广告时间:

正文在此开始。

Vue2的proxy实现

众所周知,Vue2中实现代理的方式是通过数据劫持来实现的,也就是使用的Object.defineProperty;简单举例如下:

var obj = {};
var initValue = 20;

Object.defineProperty(obj, "age", {
  get: function () {
    console.log('get')
    return initValue;
  },
  set: function (value) {
    console.log('set')
    initValue = value;
  }
});

console.log(obj.age);
obj.age = 22;
console.log(obj.age);

如上代码,控制台会输出:

在Vue2中其实就是这么来实现的数据劫持,其中get里面会收集依赖--depend,set里面会触发依赖--notify;vue-defineReactive源码直达

当然还有数组的处理,因为数组是个比较特殊的数据类型,Vue2中对数组的方法进行了重新封装,改变原始数组数据的方法都被重新封装了,如下:push、pop、shift、unshift、splice、sort、reversevue数组重新封装源码直达

关于Vue2中的observe具体是如何实现的,在这里不做过多解读,可以看下这篇文章Vue2源码解读-Observe

Vue2的proxy存在的问题

在Vue2中,即便是对数组进行了重新封装,还是会存在问题,如下:

var list = ['tom', 'jack', 'draven', 'ifil']
// 直接改变数组的长度,Vue2是监听不到的,
list.length = 3;
// 直接改变数组中的某个元素,Vue2中也是监听不到的,
list[2] = 'luckyDraven';

虽然Vue2中对上面代码中对数组的修改方式提供了Vue.$set方法去弥补,但是对于开发人员来说,也是增加了额外的工作量嘛。关于这部分内容Vue2的官方文档也进行了说明。关于数组

Vue3的Proxy

Vue3抛弃了数据劫持,转而使用的是Proxy+Reflect来实现的数据代理。

什么是Proxy

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。Proxy 的构造函数语法为:

const p = new Proxy(target, handler)
  • target:要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
  • handler:一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。

handler可以包含的方法(也叫捕捉器)如下:

// Object.getPrototypeOf 方法的捕捉器。
handler.getPrototypeOf()

// Object.setPrototypeOf 方法的捕捉器。
handler.setPrototypeOf()

// Object.isExtensible 方法的捕捉器。
handler.isExtensible()

// Object.preventExtensions 方法的捕捉器。
handler.preventExtensions()

// Object.getOwnPropertyDescriptor 方法的捕捉器。
handler.getOwnPropertyDescriptor()

// Object.defineProperty 方法的捕捉器。
handler.defineProperty()

// in 操作符的捕捉器。
handler.has()

// 属性读取操作的捕捉器。
handler.get()

// 属性设置操作的捕捉器。
handler.set()

// delete 操作符的捕捉器。
handler.deleteProperty()

// Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。
handler.ownKeys()

// 函数调用操作的捕捉器。
handler.apply()

// new 操作符的捕捉器。
handler.construct()

相关的参数说明如下:

  • target:要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
  • handler:一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。

举个官方例子:

const handler = {
    get: function(obj, prop) {
        return prop in obj ? obj[prop] : 37;
    }
};

const p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;

console.log(p.a, p.b);      // 1, undefined
console.log('c' in p, p.c); // false, 37

在上面简单的例子中,当对象中不存在属性名时,默认返回值为 37。上面的代码以此展示了 get handler 的使用场景。详细描述可以移步官网Proxy

什么是Reflect

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与 proxy handlers 的方法相同。Reflect 不是一个函数对象,因此它是不可构造的。

与大多数全局对象不同Reflect并非一个构造函数,所以不能通过new运算符对其进行调用,或者将Reflect对象作为一个函数来调用。Reflect的所有属性和方法都是静态的(就像Math对象)。

Reflect 对象提供了以下静态方法,这些方法与proxy handler methods的命名相同.

语法:


// 对一个函数进行调用操作,同时可以传入一个数组作为调用参数。
// 和 Function.prototype.apply() 功能类似。
Reflect.apply(target, thisArgument, argumentsList)

// 对构造函数进行 new 操作,相当于执行 new target(...args)。
Reflect.construct(target, argumentsList[, newTarget])

// 和 Object.defineProperty() 类似。如果设置成功就会返回 true
Reflect.defineProperty(target, propertyKey, attributes)

// 作为函数的delete操作符,相当于执行 delete target[name]。
Reflect.deleteProperty(target, propertyKey)

// 获取对象身上某个属性的值,类似于 target[name]。
Reflect.get(target, propertyKey[, receiver])

// 类似于 Object.getOwnPropertyDescriptor()。
// 如果对象中存在该属性,则返回对应的属性描述符,  否则返回 undefined.
Reflect.getOwnPropertyDescriptor(target, propertyKey)

// 类似于 Object.getPrototypeOf()。
Reflect.getPrototypeOf(target)

// 判断一个对象是否存在某个属性,和 in 运算符 的功能完全相同。
Reflect.has(target, propertyKey)

// 类似于 Object.isExtensible().
Reflect.isExtensible(target)

// 返回一个包含所有自身属性(不包含继承属性)的数组。
// (类似于 Object.keys(), 但不会受enumerable影响).
Reflect.ownKeys(target)

// 类似于 Object.preventExtensions()。返回一个Boolean。
Reflect.preventExtensions(target)

// 将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true。
Reflect.set(target, propertyKey, value[, receiver])

// 设置对象原型的函数. 返回一个 Boolean, 如果更新成功,则返回true。
Reflect.setPrototypeOf(target, prototype)

举个例子:

  • 检测一个对象是否存在特定属性

    const duck = {
      name: 'Maurice',
      color: 'white',
      greeting: function() {
        console.log(`Quaaaack! My name is ${this.name}`);
      }
    }
    
    Reflect.has(duck, 'color');
    // true
    Reflect.has(duck, 'haircut');
    // false
    
  • 检测一个对象是否存在特定属性

    const duck = {
      name: 'Maurice',
      color: 'white',
      greeting: function() {
        console.log(`Quaaaack! My name is ${this.name}`);
      }
    }
    Reflect.set(duck, 'eyes', 'black'); // returns "true" if successful
    
    console.log(Reflect.ownKeys(duck)); // ["name", "color", "greeting", "eyes"]
    

实现部分

阅读到这里,应该可以看到Proxy中handler部分和Reflect中所支持的静态方法是一一对应的。

Vue3中通过Proxy结合Reflect来彻底代理实现了数据代理。关于源码分析部分可以查看这篇文章Vue3源码解读-createReactiveObject

Vue3中通过不同的api来调用不同的handler实现数据代理。贴一下源码解读中的图吧:

Vue2和Vue3的Proxy对比

  • 通过Vue2中Object.defineProperty是不支持对数组的监听的,只支持对对象的监听;
  • 来看个Vue3中使用Proxy+Reflect的例子
    var list = ["tom", "jack"];
    
    var proxy = new Proxy(list, {
      set: function (target, key, value) {
        Reflect.set(target, key, value);
      }
    });
    
    proxy.length = 3;
    
    console.log(list);  // ["tom", "jack", undefined]
    
    proxy[2] = "draven";
    
    console.log(list); // ["tom", "jack", "draven"]
    

可以看到通过Proxy+Reflect实现了Vue2中length和直接赋值监听不到的问题。

the last

当然了Proxy的优势不止上面数组的部分,还有一些其他的优势,可以去官网进行深度阅读。

大家都喜欢新鲜的东西,其实重要的不是结果,而是探索的过程~

感谢阅读,祝大家开工大吉~

觉得不错的话,点个star再走哇~

vite解析 + 搭建vite-pro大型公司MIS项目实践,真香

hello,大家好,我是德莱问,今天为大家带来vite解析。

最后提供一个使用vite+react+concent的一个后台项目。

写在前面的话

vite,号称是下一代前端开发和构建工具。vite的出现得益于浏览器对module的支持,利用浏览器的新特性去实现了极速的开发体验;能够极快的实现热重载(hmr).

开发模式下,利用浏览器的module支持,达到了极致的开发体验;

正式环境的编译打包,使用了首次提出tree-shaking的rollup进行构建;

vite提供了很多的配置选项,包括vite本身的配置,esbuild的配置,rollup的配置等等,今天带领大家从源码的角度看看vite。

vite其实是可以分为三部分的,一部分是开发过程中的client部分;一部分是开发过程中的server部分;另外一部分就是与生产有关系的打包编译部分,由于vite打包编译其实是用的rollup,我们不做解析,只看前两部分。

vite-client

vite的client其实是作为一个单独的模块进行处理的,它的源码是放在packages/vite/src/client;这里面有四个文件:

  • client.ts: 主要的文件入口,下面着重介绍;
  • env.ts:环境相关的配置,这里会把我们在vite.config.js(vite配置文件)的define配置在这里进行处理;
  • overlay.ts: 这个是一个错误蒙层的展示,会把我们的错误信息进行展示;
  • tsconfig.json: 这就是ts的配置文件了。

工具部分

client里面是提供了一系列工具函数,主要是为了hmr;

image.png

websocket部分

  • 建立websocket连接
  • 调用上面的overlay进行错误展示
  • Message通信

其中message通信部分有多种事件类型,可以参见下图:

image.png

举例说明

使用vite-app创建了一个简单的demo:

yarn create @vitejs/app my-react-ts-app --template react-ts

使用以上命令,可以简单的创建一个react-ts的vite应用。

npm install
npm run dev

执行以上命令,进行安装依赖,然后启动服务,打开浏览器: http://localhost:3000/,network界面,可以看到有如下请求:

image.png

我把这几种类型的数据进行了划分,按照不同的类型进行不同的划分:

image.png

咱们接下来来分析下,html的内容:

<!DOCTYPE html>
<html lang="en">
  <head>
<script type="module" src="/@vite/client"></script>
  <script type="module">
import RefreshRuntime from "/@react-refresh"
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => {}
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
</script>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

可以看到,涉及到js的一共三块:

  • client,请求路径为/@vite/client,请注意这个路径,这是vite本身的依赖的路径;
  • react-refresh的模块代码,这是插件react-refresh注入的代码;代码内部又请求了@react-refresh,这是插件react-refresh的sdk的请求;
  • main,请求路径为/src/main/tsc,这是与咱们项目中的真实代码相关的;

除了上面的三个外,还有一个env,请求路径为/@vite/env.js,这个就是@vite/client内部发出的对env依赖的请求:import '/node_modules/vite/dist/client/env.js';;

当然还有@react-refresh的sdk请求;

除了上面所提到的js之外,其他的请求其实就是我们项目代码里面的请求了;

client第一部要做的事情就是建立websocket通信通道,可以看到上面的websocket类型的localhost请求,这就是client与server端通信,进行热更新等的管道。

vite- server

说完了client,我们回到server部分,入口文件为packages/vite/src/node/serve.ts,最主要的逻辑其实是在packages/vite/src/node/server/index.ts;我们暂且把server端称为node端,node端主要包含几种类型文件的处理,毕竟这只是个代理服务器;

image.png

我们从几个部分来看看这几种类型的处理

node watcher

watcher的主要作用是对于文件变化的监听,然后与client端进行通信:
image.png

监听的目录为整个项目的根目录,watchOptions为vite.config.js里面的server.watch配置,初始化代码如下:

// 使用chokidar进行对文件目录的监听,
  const watcher = chokidar.watch(path.resolve(root), {
    ignored: ['**/node_modules/**', '**/.git/**', ...ignored],
    ignoreInitial: true,
    ignorePermissionErrors: true,
    ...watchOptions
  }) as FSWatcher

启动对文件的监听:

// 如果发生改变,调用handleHMRUpdate,
  watcher.on('change', async (file) => {
    file = normalizePath(file)
    // invalidate module graph cache on file change
    moduleGraph.onFileChange(file)
    if (serverConfig.hmr !== false) {
      try {
        await handleHMRUpdate(file, server)
      } catch (err) {
        ws.send({
          type: 'error',
          err: prepareError(err)
        })
      }
    }
  })

  // 增加文件连接
  watcher.on('add', (file) => {
    handleFileAddUnlink(normalizePath(file), server)
  })

  // 减少文件连接
  watcher.on('unlink', (file) => {
    handleFileAddUnlink(normalizePath(file), server, true)
  })

监听对应的事件所对应的处理函数在packages/vite/src/node/server/hmr.ts文件里面。再细节的处理,我们不做说明了,其实里面逻辑是差不太多的,最后都是调用了websocket,发送到client端。

node 依赖类型

依赖类型,其实也就是node_modules下面的依赖包,例如:

image.png
这些包属于基本不会变的类型,vite的做法是把这些依赖,在服务启动的时候放到.vite目录下面,收到的请求直接去.vite下面获取,然后返回。

node 静态资源

静态资源其实也就是我们了解和熟悉的public/下面的或者static/下面的内容,这些资源属于静态文件,例如:

image.png
这样的数据,vite不做任何处理,直接返回。

node html

对于入口文件index.html,我们这里暂且只讲单入口文件,多入口文件vite也是支持的,详情可见多页面应用

// 删减后得代码如下
// @file packages/vite/src/node/server/middlewares/indexHtml.ts
export function indexHtmlMiddleware(server){
  return async (req, res, next) => {
    const url = req.url && cleanUrl(req.url)
    const filename = getHtmlFilename(url, server)
    try {
      // 从本地读取index.html的内容
      let html = fs.readFileSync(filename, 'utf-8')
      // dev模式下调用createDevHtmlTransformFn转换html的内容,插入两个script
      html = await server.transformIndexHtml(url, html)
      // 把html的内容返回。
      return send(req, res, html, 'html')
    } catch (e) {
      return next(e)
    }
  }
}

对于入口文件index.html,vite首先会从硬盘上读取文件的内容,经过一系列操作后,把操作后的内容进行返回,我们来看看这个一系列操作:

  • 调用createDevHtmlTransformFn去获取处理函数:
// @file packages/vite/src/node/plugins/html.ts
export function resolveHtmlTransforms(plugins: readonly Plugin[]) {
  const preHooks: IndexHtmlTransformHook[] = []
  const postHooks: IndexHtmlTransformHook[] = []

  for (const plugin of plugins) {
    const hook = plugin.transformIndexHtml
    if (hook) {
      if (typeof hook === 'function') {
        postHooks.push(hook)
      } else if (hook.enforce === 'pre') {
        preHooks.push(hook.transform)
      } else {
        postHooks.push(hook.transform)
      }
    }
  }

  return [preHooks, postHooks]
}

// @file packages/vite/src/node/server/middlewares/indexHtml.ts
export function createDevHtmlTransformFn(server: ViteDevServer) {
  const [preHooks, postHooks] = resolveHtmlTransforms(server.config.plugins)
  return (url: string, html: string): Promise<string> => {
    return applyHtmlTransforms(
      html,
      url,
      getHtmlFilename(url, server),
      [...preHooks, devHtmlHook, ...postHooks],
      server
    )
  }
}

此处,我们还是拿react项目为例,react-refresh的插件被插入到了postHooks里面;最后其实是返回了一个无名的promise类型的函数;此处也就是闭包了。无名函数里面调用的是applyHtmlTransforms,我们来看下参数:

  • html为根目录下面的index.html的内容
  • url为/index.html,
  • 第三个参数的执行结果为/index.html
  • 第四个参数为一个大数组,prehooks是空的,第二个为是vite自己的/@vite/client链接的返回函数,第三个是有一个react-refresh的插件在里面的
  • 第五个参数为当前server

接下来是applyHtmlTransforms的调用时刻,此处会改写html内容,然后返回。

image.png

最后处理好的html的内容,就是我们上面看到的html的内容。

node 其他类型

暂时把其他类型都算为其他类型,包括@Vite开头的/@vite/client和业务相关的请求;这些请求都会走同一个transformMiddleware中间件。此中间件所做的工作如下:

// @file packages/vite/src/node/server/middlewares/transform.ts

image.png

其实上面的逻辑正常走下来,是会到命中缓存和未命中缓存中的,二选一,命中就直接返回了,没有命中的话,就是走到了transform,接下来我们看下调用transform的过程:

// @file packages/vite/src/node/server/transformRequest.ts
// 调用插件获取当前请求的id,如/@react-refresh,当然也有获取不到的情况;
const id = (await pluginContainer.resolveId(url))?.id || url
// 调用插件获取插件返回的内容,如/@react-refresh,肯定有不是插件返回的情况,
const loadResult = await pluginContainer.load(id, ssr)
// 接下来是重点
// 如果没有获取到结果,也就是不是插件类型的请求,如我们的入口文件/src/main.tsx
if (loadResult == null) {
    // 从硬盘读取非插件提供的返回结果
    code = await fs.readFile(file, 'utf-8')
  } else {
    if (typeof loadResult === 'object') {
      code = loadResult.code
      map = loadResult.map
    } else {
      code = loadResult
    }
  }
}
// 启动文件监听,调用watcher,和上面讲到的watcher遥相呼应
ensureWatchedFile(watcher, mod.file, root)
// 代码运行到这里,是获取到内容了不假,不过code还是源文件,也就是编写的文件内容
// 下面的transform是开始进行替换
const transformResult = await pluginContainer.transform(code, id, map, ssr)
code = transformResult.code!
map = transformResult.map
return (mod.transformResult = {
      code,
      map,
      etag: getEtag(code, { weak: true })
    } as TransformResult)

大体的流程如下:

image.png

async transform(code, id, inMap, ssr) {
  const ctx = new TransformContext(id, code, inMap as SourceMap)
  ctx.ssr = !!ssr
  for (const plugin of plugins) {
    if (!plugin.transform) continue
    ctx._activePlugin = plugin
    ctx._activeId = id
    ctx._activeCode = code
    let result
    try {
      result = await plugin.transform.call(ctx as any, code, id, ssr)
    } catch (e) {
      ctx.error(e)
    }
    if (!result) continue
    if (typeof result === 'object') {
      code = result.code || ''
      if (result.map) ctx.sourcemapChain.push(result.map)
    } else {
      code = result
    }
  }
  return {
    code,
    map: ctx._getCombinedSourcemap()
  }
},

image.png

其实到这里,我们对于vite server所实现的功能基本是已经清楚了,代理服务器,然后对引用修改为自己的规则,对自己的规则进行解析处理。尤为重要的其实是 vite:import-analysis这个插件。

vite + react

开始之前先附上地址:github:vite-react-concent-pro;这个项目是由github:webpack-react-concent-pro项目改过来的,业务逻辑代码模块没动,只改动了编译打包部分。

在这里说下由webpack改为vite的过程和其中遇到的一些问题。

项目的改动其实是不大的,基本就是clone下项目下来后,把webpack相关的依赖去掉,然后换成vite,记得加上react的vite插件:@vitejs/plugin-react-refresh;换完以后,因为我们项目中的引用路径是在src文件夹下面的,所以我们需要为vite提供下别名:

resolve: {
    alias: { // 别名
      "configs": path.resolve(__dirname, 'src/configs'),
      "components": path.resolve(__dirname, 'src/components'),
      "services": path.resolve(__dirname, 'src/services'),
      "pages": path.resolve(__dirname, 'src/pages'),
      "types": path.resolve(__dirname, 'src/types'),
      "utils": path.resolve(__dirname, 'src/utils'),
    },
},

这样我们不用改动里面的引用,就可以让vite知道去哪里找哪个文件了。
引用中有对process.env.***类似的引用,用此来判断一些环境相关的逻辑,在vite中是没有了,vite的环境变量是通过import.meta.env.***;

改完这些执行npm run start,是可以正常跑起来的。

坑1

在执行npm run build后,我们在进行预览的时候,执行npm run preview,出现了下面的画面:

image.png
出现了这种没见过的错误,然后我们的解决办法是什么呢?

首先,把压缩给干掉,别压缩了,压缩后的代码全都是abcd,啥也看不出来;干掉的方式是改vite的配置:

build: {
    minify: false, // 是否进行压缩,boolean | 'terser' | 'esbuild',默认使用terser
    manifest: false, // 是否产出maifest.json
    sourcemap: false, // 是否产出soucemap.json
    outDir: 'build', // 产出目录
  },

我们把minify改为了false,再重新执行build和preview命令,可以看到了精确的行,到底是哪里进行了报错.

关于最后是怎么解决的呢?TMD,竟然是一个object-inspect的库,引用了一个util的包,然后咱们的node_modules里面没有util的包。

这些个中缘由,就不多说了,折腾了两三个小时,解决办法就是一个命令:npm i -S util

重新执行build和preview后,正常了。

坑2

本地开发启动start,build+preview都OK了,接下来,就得试试单例了。执行npm run test。果不其然,报错了,原因是没有babel-preset-react-app的babel配置。

那我们增加上配置那不就好了嘛?

我们在package.json里面增加了babel的配置:

"bable": {
    "presets": [
        "react-app"
    ],
}

接着我们运行npm run test;嗯,OK了,跑成功了。

我们再重新测试下,执行npm run start,TMD挂了,跑步起来了!!!

**
Using babel-preset-react-app requires that you specify NODE_ENV or BABEL_ENV environment variables. Valid values are "development", "test", and "production". Instead, received: undefined
**

上面这句话啥意思呢?就是我们的babel-preset-react-app这个包运行的时候需要一个process.env.NODE_ENV或者process.env.BABEL_ENV的变量。
我们本着vite不在process上面搞事情的原则,这个问题是解决不了的,也就是说,不能通过配置的方式来实现babel的配置了,那怎么整??

查了下babel-preset-react-app这个包的源码,发现是可以通过参数的形式传递进去的,所以我们得从test的时候所做的事情入手,test的时候,我们运行的是jest,jest是有它的配置文件的,叫jest.config.js;jest的配置文件里面有一个transform的对象,这个对象里面是有了babel-jest这个库,这也就是babel了。

我们得在这里搞点事情,最后经过多次调试,配置是这样的了:

// vite react项目里面单测需要在这里把babel-react-app传递进去,不可在项目中或者package.json里面配置babel
transform: {
    // vite react项目里面单测需要在这里把babel-react-app传递进去,不可在项目中或者package.json里面配置babel
    "^.+\\.(js|jsx|ts|tsx)$": ["<rootDir>/node_modules/babel-jest", {"presets": ['babel-preset-react-app'] }],
    "^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
    "^(?!.*\\.(js|jsx|ts|tsx|css|json)$)": "<rootDir>/config/jest/fileTransform.js",
  },

这就是个最大的坑,又耗费了我2个小时的时间折腾这个。

写在最后

vite已经发布了2版本,在公司内部的项目中,是可以进行使用了,由于线上线下跑的不是一套代码,尤老板还专门提供了个preview的功能,建议大家可以尝试一下。

另外上面说到的那个项目:github:vite-react-concent-pro,目前包含的功能也是比较齐全的:

  • start:本地启动开发与调试
  • build:编译打包
  • preview:预览打包完成的代码:
  • test:单测
  • snap:生成快照

该项目整合了react、concent(一个特别好用的状态管理库)、antd、react-router-dom、axios等,可以0成本接入开发。

当然了如果你的现有项目想改成vite,也是很简单的:

  • 把该项目clone下来,把src下面的内容删掉;
  • 把你的老项目的src下面的文件搬到这个项目的src文件下面,然后改改alias和process.env;
  • 记得index.html要改成你的入口文件哦

接下来就等着见证奇迹吧

使用了vite之后,npm run start能够提高80%左右npm run build能够提高50%左右

!嗯,真香~

Vue3源码分析之compositionApi

hello,大家好,我是德莱问。

本篇文章将会围绕Vue3的另外一个主要的文件夹reactivity来进行讲解,也就是Vue3中对外暴露的compositionApi的部分,,越来越有React Hooks的味道了。reactivity文件夹下面包含多个文件,主要功能在于computed、effect、reactive、ref;其他的文件是为其进行服务的,另外还有一个主入口文件index。reactivity下面对外暴露的所有api可见下图,我们本篇文件会结合使用对这些功能进行源码分析。

正文

正文在这里,正式开始。

computed

computed的含义与Vue2中的含义是一样的,计算属性;使用方式也是和Vue2中差不多的,有两种使用方式:

computed使用

const {reactive, readonly, computed, ref} = Vue;

const app = Vue.createApp({});
app.component('TestComponent', {
    setup(props) {
        // reactive
        const state = reactive({
            count: 0,
            number: 10
        })
        // computed getter
        const computedCount = computed(() => {
            return state.count + 10
        })
        // computed set get
        const computedNumber = computed({
            get: () => {
                return state.number + 100
            },
            set: (value) => {
                state.number = value - 50
            }
        })

        const changeCount = function(){
            state.count++;
            computedNumber.value = 200
        }
        return {
            state,
            changeCount,
            computedCount,
            computedNumber
        }
    },
    template: `
        <div>
            <h2>init count:<i>{{state.count}}</i></h2>
            <h2>computedCount:<i>{{computedCount}}</i></h2>
            <h2>computedNumber:<i>{{computedNumber}}</i></h2>
            <button @click="changeCount">changeCount</button>
        </div>
    `
})

app.mount('#demo')

上面代码可以看到两次对computed的使用,第一次传递的是一个函数,第二次传递的是一个包含get和set的对象。

computed源码分析

接下来,咱们来看下computed的源码:

// @file packages/reactivity/src/computed.ts
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
    setter = __DEV__
      ? () => {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  return new ComputedRefImpl(
    getter,
    setter,
    isFunction(getterOrOptions) || !getterOrOptions.set
  ) as any
}

上面是computed的入口的源码,此处和Vue2中的写法是一样的,都是对参数进行判断,生成getter和setter,这里最后调用的是ComputedRefImpl;

// packages/reactivity/src/computed.ts
class ComputedRefImpl<T> {
  private _value!: T
  private _dirty = true

  public readonly effect: ReactiveEffect<T>

  public readonly __v_isRef = true;
  public readonly [ReactiveFlags.IS_READONLY]: boolean

  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean
  ) {
    this.effect = effect(getter, {
      lazy: true,
      scheduler: () => {
        if (!this._dirty) {
          this._dirty = true
          trigger(toRaw(this), TriggerOpTypes.SET, 'value')
        }
      }
    })

    this[ReactiveFlags.IS_READONLY] = isReadonly
  }

  get value() {
    if (this._dirty) {
      this._value = this.effect()
      this._dirty = false
    }
    track(toRaw(this), TrackOpTypes.GET, 'value')
    return this._value
  }

  set value(newValue: T) {
    this._setter(newValue)
  }
}

如上,是ComputedRefImpl的源码。ComputedRefImpl是一个class,内部包含_value、_dirty、effect、__v_isRef、ReactiveFlags.IS_READONLY等属性,还包括constructor和get、set等函数。了解的同学都知道,会首先调用构造函数也就是constructor;调用effect为effect属性赋值,把isReadonly赋值给ReactiveFlags.IS_READONLY属性,关于effect,咱们后面讲这块。此时ComputedRefImpl执行完成。

当获取当前computed的值的时候,如上面使用中computedCount在template中进行获取值的时候,会调用上面class内的get方法,get方法内部调用的是this.effect进行数据的获取,_dirty属性是为了数据的缓存,依赖未发生变化,则不会调用effect,使用之前的value进行返回。track是跟踪当前get调用的轨迹。

当为computed赋值的时候,如上面使用中computedNumber.value = 200的时候,,会调用上面class内的set方法,set内部还是调用了之前传递进来的函数。

reactive

接下来对reactive的讲解

reactive 使用

reactive官网给的解释是:返回对象的响应式副本。先来看下reactive的使用

const {reactive} = Vue;

const app = Vue.createApp({});
app.component('TestComponent', {
    setup(props) {
        // reactive
        const state = reactive({
            count: 0
        })
        const changeCount = function(){
            state.count++;
        }
        return {
            state,
            changeCount
        }
    },
    template: `
        <div>
            <h2>reactive count:<i>{{state.count}}</i></h2>
            <button @click="changeCount">changeCount</button>
        </div>
    `
})
app.mount('#demo')

当点击changeCount的时候,state.count会++,同时映射到h2-dom。

reactive 源码解读

// @file packages/reactivity/src/reactive.ts
export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers
  )
}

如上源码可以看到,如果target有值并且target的[ReactiveFlags.IS_READONLY]属性,也就是__v_isReadonly为true的话,会直接返回当前对象,不做任何处理,后面对state.count的改变也不会映射到dom当中。如果不满足上面条件,则会调用createReactiveObject函数,传递4个参数:

  • target为原始对象;
  • 第二个是isReadonly,为false;
  • 第三个参数mutableHandlers是reactive对应的处理函数;
  • 第四个参数是对于集合类型的对象进行处理的函数。

关于这个核心的函数,我们待会来进行讲解。

readonly 使用

现在我们来看下Vue3提供给我们的reactivity下面的第二个api:readonly。

官网给出的定义是:获取一个对象 (响应式或纯对象) 或 ref 并返回原始代理的只读代理。只读代理是深层的:访问的任何嵌套 property 也是只读的

const {readonly} = Vue;

const app = Vue.createApp({});
app.component('TestComponent', {
    setup(props) {
        const read = readonly({count: 1})

        const changeRead = function(){
            read.count++;
        }
        return {
            read,
            changeRead
        }
    },
    template: `
        <div>
            <h2>readonly count:<i>{{read.count}}</i></h2>
            <button @click="changeRead">changeRead</button>
        </div>
    `
})

app.mount('#demo')

上面代码,是readonly的使用,在此试验了一下对readonly返回后的结果read,进行了改变的尝试,发现是改变不了的,属于只读,同时还会打印警告Set operation on key "count" failed: target is readonly.

readonly 源码解读

// @file packages/reactivity/src/reactive.ts
export function readonly<T extends object>(
  target: T
): DeepReadonly<UnwrapNestedRefs<T>> {
  return createReactiveObject(
    target,
    true,
    readonlyHandlers,
    readonlyCollectionHandlers
  )
}

上面就是readonly的源码入口,与reactive一样,都是调用的createReactiveObject函数:

  • 第一个参数还是target;
  • 第二个是isReadonly,为true;
  • 第三个参数readonlyHandlers是readonly对应的处理函数;
  • 第四个参数是对于集合类型的对象进行处理的readonly所对应的函数。

shallowReactive 使用

官网文档给的解释:创建一个响应式代理,该代理跟踪其自身 property 的响应性,但不执行嵌套对象的深度响应式转换 (暴露原始值)。来看下shallowReactive的使用

const {shallowReactive} = Vue;

const app = Vue.createApp({});
app.component('TestComponent', {
    setup(props) {
        const state = shallowReactive({
            foo: 1,
            nested: {
                bar: 2
            }
        })
        const change = function(){
            state.foo++
            state.nested.bar++
        }
        return {
            state,
            change
        }
    },
    template: `
        <div>
            <h2>foo:<i>{{state.foo}}</i></h2>
            <h2>bar:<i>{{state.nested.bar}}</i></h2>
            <button @click="change">change</button>
        </div>
    `
})

app.mount('#demo')

上面代码基本是完全按照官网来写的,不过,试了下效果和官网上的效果不一样,并不是shallow类型,而是对内部的属性也进行了监听,bar的改变也会响应式的反映到dom当中去。也不知道是我姿势不对,还是Vue3的bug。

shallowReactive 源码解读

// @file packages/reactivity/src/reactive.ts
export function shallowReactive<T extends object>(target: T): T {
  return createReactiveObject(
    target,
    false,
    shallowReactiveHandlers,
    shallowCollectionHandlers
  )
}

上面就是shallowReactive的源码入口,与reactive和readonly一样,都是调用的createReactiveObject函数:

  • 第一个参数还是target;
  • 第二个是isReadonly,为false;
  • 第三个参数shallowReactiveHandlers是shallowReactive对应的处理函数;
  • 第四个参数是对于集合类型的对象进行处理的shallowReactive所对应的函数。

shallowReadonly 使用

官网给出的解释:创建一个代理,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换 (暴露原始值)。来看下使用:

const {shallowReadonly} = Vue;

const app = Vue.createApp({});
app.component('TestComponent', {
    setup(props) {
        const state = shallowReadonly({
            foo: 1,
            nested: {
                bar: 2
            }
        })
        const change = function(){
            state.foo++
            state.nested.bar++
        }
        return {
            state,
            change
        }
    },
    template: `
        <div>
            <h2>foo:<i>{{state.foo}}</i></h2>
            <h2>bar:<i>{{state.nested.bar}}</i></h2>
            <button @click="change">change</button>
        </div>
    `
})

app.mount('#demo')

上面代码基本是完全按照官网来写的,foo的改变不被允许,按照官网说明state.nested.bar是允许被改变的,在上面例子中,发现state.nested.bar的值是会改变的,但是不会响应到dom上。

shallowReadonly 源码解读

// @file packages/reactivity/src/reactive.ts
export function shallowReadonly<T extends object>(
  target: T
): Readonly<{ [K in keyof T]: UnwrapNestedRefs<T[K]> }> {
  return createReactiveObject(
    target,
    true,
    shallowReadonlyHandlers,
    readonlyCollectionHandlers
  )
}

上面就是shallowReadonly的源码入口,与reactive和readonly一样,都是调用的createReactiveObject函数:

  • 第一个参数还是target;
  • 第二个是isReadonly,为true;
  • 第三个参数shallowReadonlyHandlers是shallowReadonly对应的处理函数;
  • 第四个参数是对于集合类型的对象进行处理的shallowReadonly所对应的函数。

isReadonly

isReadonly:检查对象是否是由readonly创建的只读代理。

使用如下:

const only = readonly({
	count: 1
})
isOnly = isReadonly(only) // true

源码如下:

export function isReadonly(value: unknown): boolean {
  return !!(value && (value as Target)[ReactiveFlags.IS_READONLY])
}

ReactiveFlags.IS_READONLY是一个字符串,值为:__v_isReadonly,挂到对象上面就是属性,判断当前对象的__v_isReadonly属性是否是true,并返回。

isReactive

isReadonly:检查对象是否是 reactive创建的响应式 proxy。

使用如下:

const tive = reactive({
	count: 1
})
isOnly = isReactive(tive) // true

源码如下:

export function isReactive(value: unknown): boolean {
  if (isReadonly(value)) {
    return isReactive((value as Target)[ReactiveFlags.RAW])
  }
  return !!(value && (value as Target)[ReactiveFlags.IS_REACTIVE])
}

首先调用了上面提到的isReadonly方法判断是否是readonly创建的对象;如果是的话,则进一步使用当前对象的RAW属性调用isReactive来判断;如果不是则判断__v_isReactive是否为true;返回判断的结果。

ReactiveFlags.RAW是一个字符串,值为:__v_raw,挂到对象上面就是属性,也就是原始对象,判断是否是reactive代理的原始对象;
ReactiveFlags.IS_READONLY也是一个字符串,值为:__v_isReactive,挂到对象上面就是属性

isProxy

isProxy:检查对象是否是reactive 或 readonly创建的代理。

使用如下:

const tive = reactive({
	count: 1
})
const only = readonly({
	count: 1
})
is1 = isProxy(tive) // true
is2 = isProxy(only) // true

源码如下:

export function isProxy(value: unknown): boolean {
  return isReactive(value) || isReadonly(value)
}

调用上面提到的isReadonly方法和isReactive判断是否是proxy的对象。

markRaw

markRaw:标记一个对象,使其永远不会转换为代理。返回对象本身。

使用如下:

const foo = markRaw({})
console.log(isReactive(reactive(foo))) // false

const bar = reactive({ foo })
console.log(isReactive(bar)) // true
console.log(isReactive(bar.foo)) // false

从上面使用中可以看到,markRaw只对当前对象本身有效,被标记的对象作为属性的时候,大对象bar还是可以进行响应式处理的,但是bar里面的当前被标记的对象foo,还是一个非响应式对象,永远是foo对象本身。

export function markRaw<T extends object>(value: T): T {
  def(value, ReactiveFlags.SKIP, true)
  return value
}
export const def = (obj: object, key: string | symbol, value: any) => {
  Object.defineProperty(obj, key, {
    configurable: true,
    enumerable: false,
    value
  })
}

上面可以看到markRaw的源码,就是给要标记的对象增加了一个属性(ReactiveFlags.SKIP, 也就是__v_skip),并赋值true,所有要给当前对象进行响应式处理的时候,都会被忽略。

toRaw

toRaw:返回 reactive 或 readonly 代理的原始对象。这是一个转义口,可用于临时读取而不会引起代理访问/跟踪开销,也可用于写入而不会触发更改。不建议保留对原始对象的持久引用。请谨慎使用。

既然Vue让咱们谨慎使用,咱们还是在可以不使用的的时候不使用的好,这个就是把代理的原始对象进行返回。

const obj = {
    project: 'reactive'
}
const reactiveObj = reactive(obj)

console.log(toRaw(reactiveObj) === obj) // true

源码:

export function toRaw<T>(observed: T): T {
    return (
        (observed && toRaw((observed as Target)[ReactiveFlags.RAW])) || observed
    )
}

返回当前对象的ReactiveFlags.RAW(也就是__v_raw)属性指向的对象,也就是对象本身,关于在什么地方给ReactiveFlags.RAW赋值的,后面会看到这部分。

createReactiveObject

上面的reactive、readonly、shallowReactive、shallowReadonly,都用到了createReactiveObject函数,现在咱们来看看这个函数的源码。

function createReactiveObject(target: Target, isReadonly: boolean, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any>) {
    if (!isObject(target)) {
        if (__DEV__) {
            console.warn(`value cannot be made reactive: ${String(target)}`)
        }
        return target
    }
    if (target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE])) {
        return target
    }
    const proxyMap = isReadonly ? readonlyMap : reactiveMap
    const existingProxy = proxyMap.get(target)
    if (existingProxy) {
        return existingProxy
    }
    const targetType = getTargetType(target)
    if (targetType === TargetType.INVALID) {
        return target
    }
    const proxy = new Proxy(
        target,
        targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
    )
    proxyMap.set(target, proxy)
    return proxy
}

源码解读:

  • 首先,target得是一个对象,不是对象的话,直接返回当前值;当然Vue3也提供了对值的响应式的方法:ref,后面讲。
  • 判断有原始对象且,不是只读或者不是响应式的对象,则返回当前对象,这个地方真TM绕。
  • 根据是否是isReadonly,获取到代理存储的map,,如果之前代理过,已经存在,则把之前代理过的proxy返回。
  • 判断target的类型,getTargetType内部会对target对象进行判断,返回是common、collection或者invalid;如果不可用类型(invalid),则直接返回当前对象。此处会用到上面讲到的__v_skip。可用的类型就两个,一个是common,一个是collection;
  • 接下来就是没有代理过,获取代理的过程。new Proxy,如果是collection则使用传递进来的collectionHandlers,否则(也就是common)则使用baseHandlers;
  • 代理存储所使用的map,存储当前proxy;
  • 返回当前proxy。

通过上面reactive、readonly、shallowReactive、shallowReadonly的讲解,可以看到对于集合和common类型,提供了几种不同的处理对象,对象中所包含的内容也是不一样的,咱们在这里来对比着看下:

basehandler:


如上图,可以看到,basehandler里面所提供的函数,我们一一来看下。

deleteProperty

// @file packages/reactivity/src/baseHandlers.ts
function deleteProperty(target: object, key: string | symbol): boolean {
  const hadKey = hasOwn(target, key)
  const oldValue = (target as any)[key]
  const result = Reflect.deleteProperty(target, key)
  if (result && hadKey) {
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}
  • 获取当前对象是否有当前key => hadKey;
  • 获取到当前的value存储为oldValue;
  • 调用Reflect.deleteProperty进行对当前对象target删除当前key的操作,返回结果为是否删除成功->result;
  • 删除成功,并且有当前key,则调用trigger,触发effect。
  • 返回删除是否成功的结果。

ownKeys

// @file packages/reactivity/src/baseHandlers.ts
function ownKeys(target: object): (string | number | symbol)[] {
  track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
  return Reflect.ownKeys(target)
}

这个函数很简单了就,获取target对象自己的属性key;跟踪获取的轨迹,然后调用Reflect.ownKeys获取结果。

has

// @file packages/reactivity/src/baseHandlers.ts
function has(target: object, key: string | symbol): boolean {
  const result = Reflect.has(target, key)
  if (!isSymbol(key) || !builtInSymbols.has(key)) {
    track(target, TrackOpTypes.HAS, key)
  }
  return result
}
  • 调用Reflect.has获取当前对象是否有当前key;
  • 不是Symbol类型的key,或者不是Symbol本身的属性,调用track跟踪has调用的轨迹。
  • 返回结果,result。

createSetter

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    const oldValue = (target as any)[key]
    if (!shallow) {
      value = toRaw(value)
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value
        return true
      }
    } else {}

    const hadKey = isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver)
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

函数工厂,根据shallow生成set函数。set函数接受4个参数:target为目标对象;key为设置的属性;value为设置的值;receiver为Reflect的额外参数(如果遇到 setter,receiver则为setter调用时的this值)。

  • 首先获取到oldValue;
  • 如果非浅响应式,也就是正式情况的时候,获取到value的原始对象并赋值给value,如果target对象不是数组且oldValue是ref类型的响应式类型,并且新value不是ref类型的响应式,为oldValue赋值(ref类型的响应式对象,需要为对象的value赋值)。
  • 下面也就是深度响应式的代码逻辑了。
  • 如果是数组并且key是数字类型的,则直接判断下标,否则调用hasOwn获取,是否包含当前key => hadKey;
  • 调用Reflect.set进行设置值;
  • 如果目标对象和receiver的原始对象相等,则hadKey,调用trigger触发add操作;否则,调用trigger触发set操作。
  • 把set处理的结果返回,result。

createGetter

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (
      key === ReactiveFlags.RAW &&
      receiver === (isReadonly ? readonlyMap : reactiveMap).get(target)
    ) {
      return target
    }

    const targetIsArray = isArray(target)
    if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver)
    }

    const res = Reflect.get(target, key, receiver)

    const keyIsSymbol = isSymbol(key)
    if (
      keyIsSymbol
        ? builtInSymbols.has(key as symbol)
        : key === `__proto__` || key === `__v_isRef`
    ) {
      return res
    }

    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }

    if (shallow) {
      return res
    }

    if (isRef(res)) {
      const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
      return shouldUnwrap ? res.value : res
    }

    if (isObject(res)) {
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

函数工厂,根据shallow生成get函数。get函数接受3个参数:target为目标对象;key为设置的属性;receiver为Reflect的额外参数(如果遇到 setter,receiver则为setter调用时的this值)。

  • 如果key是__v_isReactive,则直接返回!isReadonly,通过上面的图可得知,reactive相关的调用createGetter,传递的是false,也就是会直接返回true;
  • 如果key是__v_isReadonly,则直接返回isReadonly,同样的通过上面的图可以得知,readonly相关的调用createGetter,传递的是true,也就是会直接返回true;
  • 如果key是__v_raw并且receiver等于proxyMap存储的target对象的proxy,也就是获取原始对象,则直接返回target;
  • 如果是数组的话,则会走自定义的方法,arrayInstrumentations;arrayInstrumentations是和Vue2中对数组的改写是一样的逻辑;
  • 下面会对key进行判断,如果Symbol对象并且是Set里面自定义的方法;或者key为__proto__或__v_isRef,则直接把Reflect.get(target, key, receiver)获取到的值直接返回;
  • 如果非只读情况下,调用track跟踪get轨迹;
  • 如果是shallow,非深度响应式,也是直接把上面获取到的res直接返回;
  • 如果是ref对象,则会调用.value获取值进行返回;
  • 剩下的情况下,如果得到的res是个对象,则根据isReadonly调用readonly或reactive获取值,进行返回;
  • 最后有一个res保底返回;

collectionHandler:


来看下createInstrumentationGetter的源码,上面图中三个都是调用此方法生成对应的处理对象。

function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
  const instrumentations = shallow
    ? shallowInstrumentations
    : isReadonly
      ? readonlyInstrumentations
      : mutableInstrumentations

  return (
    target: CollectionTypes,
    key: string | symbol,
    receiver: CollectionTypes
  ) => {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (key === ReactiveFlags.RAW) {
      return target
    }

    return Reflect.get(
      hasOwn(instrumentations, key) && key in target
        ? instrumentations
        : target,
      key,
      receiver
    )
  }
}

上面createInstrumentationGetter函数根据isReadonly和shallow返回一个函数;

  • 根据isReadonly和shallow,获取到对应的instrumentations;此对象包含了对集合操作的所有方法;
  • 然后就把下面的函数进行了返回,createInstrumentationGetter相当于是一个闭包;
  • 返回的函数里面在执行调用的时候,会先对key进行判断,如果访问的是Vue的私有变量,也就是上面的__v_isReactive、__v_isReadonly、__v_raw等,会直接给出不同的返回;
  • 如果不是Vue的上面的三个私有变量,则会调用Reflect.get来获取对象的值;instrumentations,也就是重写的方法集合,不在此集合里面的,则会直接调用target自己的方法。

reactive完结

至此,reactive文件里面的这些方法咱们都梳理了一遍,简单的做了源码的分析和解读,感兴趣的读者可以深入源码研究下Vue中为何这样实现。

Refs

接下来我们将开始对ref及其附属方法的使用和讲解。

ref

首先,咱们对ref进行讲解,官网给出的解释是:接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象具有指向内部值的单个 property .value。

先来看下ref的使用。

const {ref} = Vue;

const app = Vue.createApp({});
app.component('TestComponent', {
    setup(props) {
        const count = ref(0)
        const obj = ref({number: 10})
        const change = function(){
            count.value++;
            obj.value.number++
        }

        return {
            count,
            obj,
            change
        }
    },
    template: `
        <div>
            <h2>count:<i>{{count}}</i></h2>
            <h2>number:<i>{{obj.number}}</i></h2>
            <button @click="change">change</button>
        </div>
    `
})
app.mount('#demo')

上面是ref的使用,可以看到ref接受的是一个普通类型的值或者是一个对象,Vue官网给出的例子是不包含传递对象的,其实这也就是Vue不提倡使用ref来响应式一个对象,如果是对对象的响应式,Vue还是提倡使用上面reactive来实现;第二个需要注意点在于template中对ref对象的引用是不需要加上value属性来获取值,如上ref对象count在js中需要count.value,但是在template种只需count即可

来看下ref的源码实现

// @file packages/reactivity/src/ref.ts
export function ref<T extends object>(
  value: T
): T extends Ref ? T : Ref<UnwrapRef<T>>
export function ref<T>(value: T): Ref<UnwrapRef<T>>
export function ref<T = any>(): Ref<T | undefined>
export function ref(value?: unknown) {
  return createRef(value)
}

function createRef(rawValue: unknown, shallow = false) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

class RefImpl<T> {
  private _value: T

  public readonly __v_isRef = true

  constructor(private _rawValue: T, private readonly _shallow = false) {
    this._value = _shallow ? _rawValue : convert(_rawValue)
  }

  get value() {
    track(toRaw(this), TrackOpTypes.GET, 'value')
    return this._value
  }

  set value(newVal) {
    if (hasChanged(toRaw(newVal), this._rawValue)) {
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
    }
  }
}

const convert = <T extends unknown>(val: T): T =>
  isObject(val) ? reactive(val) : val
  
export const hasChanged = (value: any, oldValue: any): boolean =>
  value !== oldValue && (value === value || oldValue === oldValue)

上面是按照运行轨迹来看的Vue3中ref的源码部分;根据ref的声明可以看到ref接受任何参数,返回类型为Ref对象,内部调用的是createRef;

  • createRef函数内部会先对value进行判断,如果已经是ref对象的话,直接返回当前value,否则就调用new RefImpl来生成ref对象进行返回。
  • constructor里面会判断是否是浅响应_shallow,浅的话,直接返回_rawValue,否则调用convert来返回;可以看到除了私有属性_value外,还有一个__v_isRef的只读属性为true;
  • convert里面则会对val进行判断了,对象则调用reactive,否则直接返回val,此处也就可以看到上面ref也可以接受对象作为参数的缘由了。
  • get里面会跟踪调用轨迹,track;返回当前value;
  • set里面会调用hasChanged判断是否发生了改变,此处会对NaN进行check,因为NaN与啥都不相等;设置新的值,同时调用trigger触发set调用。

isRef

isRef很明显就是判断是否是ref对象的方法。使用如下:

const count = ref(0)
const is = isRef(count)
const is2 = isRef(10)

来看下源码,源码也很简单:

export function isRef<T>(r: Ref<T> | unknown): r is Ref<T>
export function isRef(r: any): r is Ref {
  return Boolean(r && r.__v_isRef === true)
}

此处就使用到了RefImpl里面那个只读属性了,判断__v_isRef是否为true就可以了。

shallowRef

官网给出的解释:创建一个 ref,它跟踪自己的 .value 更改,但不会使其值成为响应式的。
shallowRef的源码如下:

export function shallowRef<T extends object>(
  value: T
): T extends Ref ? T : Ref<T>
export function shallowRef<T>(value: T): Ref<T>
export function shallowRef<T = any>(): Ref<T | undefined>
export function shallowRef(value?: unknown) {
  return createRef(value, true)
}

shallowRef与ref的调用流程是一样的,不过是多了个参数,导致_shallow为true,就在RefImpl里面调用时,直接返回了当前value,而不会进行到convert函数。

unRef

官网解释:如果参数为 ref,则返回内部值,否则返回参数本身。 源码如下:

export function unref<T>(ref: T): T extends Ref<infer V> ? V : T {
  return isRef(ref) ? (ref.value as any) : ref
}

确实如官网所说,就一行代码,ref对象则返回其value,否则直接返回ref。

triggerRef

官网给出的解释:手动执行与 shallowRef 关联的任何效果。 ,比较模糊,通俗点就是手动触发一次effect的调用;
看下使用:

const count = ref(0)
const change = function(){
    count.value++;
    triggerRef(count)
}
const shallow = shallowRef({
    greet: 'Hello, world'
})
watchEffect(() => {
    console.log(count.value)
    console.log(shallow.value.greet)
})
shallow.value.greet = 'Hello, universe'

源码如下:

export function triggerRef(ref: Ref) {
  trigger(ref, TriggerOpTypes.SET, 'value', __DEV__ ? ref.value : void 0)
}

toRef

官网给出的解释是:可以用来为源响应式对象上的 property 属性创建一个 ref。然后可以将 ref 传递出去,从而保持对其源 property 的响应式连接。 简单描述就是为对象的一个属性增加一个引用,这个引用可以随意使用,响应式不变。来看下源码:

export function toRef<T extends object, K extends keyof T>(
  object: T,
  key: K
): Ref<T[K]> {
  return isRef(object[key])
    ? object[key]
    : (new ObjectRefImpl(object, key) as any)
}

class ObjectRefImpl<T extends object, K extends keyof T> {
  public readonly __v_isRef = true

  constructor(private readonly _object: T, private readonly _key: K) {}

  get value() {
    return this._object[this._key]
  }

  set value(newVal) {
    this._object[this._key] = newVal
  }
}

这部分的代码比较简单,也比较容易读懂,和上面RefImpl一样的是都增加了一个只读的__v_isRef属性。

toRefs

官网对toRefs给出的解释是:将响应式对象转换为普通对象,其中结果对象的每个 property 都是指向原始对象相应 property 的ref。 通俗点描述就是把响应式对象的每个属性,都变成ref对象。来看下源码:

export function toRefs<T extends object>(object: T): ToRefs<T> {
  if (__DEV__ && !isProxy(object)) {
    console.warn(`toRefs() expects a reactive object but received a plain one.`)
  }
  const ret: any = isArray(object) ? new Array(object.length) : {}
  for (const key in object) {
    ret[key] = toRef(object, key)
  }
  return ret
}

这里尤为要求是一个响应式的对象,非响应式对象还会打印警告。for循环调用上面讲到的toRef函数,把对象里面的每个属性都变为ref对象。

customRef

官网给出的解释是:创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制。它需要一个工厂函数 来看下customRef的源码:

class CustomRefImpl<T> {
  private readonly _get: ReturnType<CustomRefFactory<T>>['get']
  private readonly _set: ReturnType<CustomRefFactory<T>>['set']

  public readonly __v_isRef = true

  constructor(factory: CustomRefFactory<T>) {
    const { get, set } = factory(
      () => track(this, TrackOpTypes.GET, 'value'),
      () => trigger(this, TriggerOpTypes.SET, 'value')
    )
    this._get = get
    this._set = set
  }

  get value() {
    return this._get()
  }

  set value(newVal) {
    this._set(newVal)
  }
}

export function customRef<T>(factory: CustomRefFactory<T>): Ref<T> {
  return new CustomRefImpl(factory) as any
}

相对应的,使用的时候,接受的是一个factory,factory是一个函数,参数为track和trigger,同时factory的返回须包含两个函数,一个为get,一个为set。track就是effect的track,trigger也是effect的trigger;来看下使用:

const {customRef} = Vue;

const app = Vue.createApp({});
function useDebouncedRef(value, delay = 200) {
    let timeout
    return customRef((track, trigger) => {
        return {
            get() {
                track()
                return value
            },
            set(newValue) {
                clearTimeout(timeout)
                timeout = setTimeout(() => {
                    value = newValue
                    trigger()
                }, delay)
            }
        }
    })
}

app.component('TestComponent', {
    setup(props) {
        return {
            text: useDebouncedRef('hello')
        }
    },
    template: `
        <div>
            <input v-model="text" />
        </div>
    `
})

app.mount('#demo')

上面是customRef的使用的例子,和官网的例子是一样的,能够实现防抖,同时也能够显式的控制什么时候调用track来跟踪和什么时候来调用trigger来触发改变。

Refs完结

上面我们对refs里面的几种方法做了源码的解读和部分的api是如何使用的,关于Vue3为何提供了两种响应式的方案:reactive和Refs,这其实就和代码风格有关系了,有的同学习惯使用对象,而有的同学习惯使用变量,Vue3为这两种方案都提供了,想用哪个用哪个。

effect

其实可以看到上面好多地方都用到了这个方法,包括effect、track、trigger等都是effect里面提供的方法,effect里面提供的方法属于Vue的内部方法,不对外暴露。下面我们挨个来看看这部分的源码,

isEffect

isEffect是为判断是否是有副作用的函数。来看下源码:

export function isEffect(fn: any): fn is ReactiveEffect {
  return fn && fn._isEffect === true
}

可以看到上面的判断,就是对函数的_isEffect进行判断,非常简单。

effect

effect作为Vue2和Vue3中核心的部分,都有这个的概念,重中之重,来看下这部分的源码:

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  if (isEffect(fn)) {
    fn = fn.raw
  }
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) {
    effect()
  }
  return effect
}

function createReactiveEffect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(): unknown {
    if (!effect.active) {
      return options.scheduler ? undefined : fn()
    }
    if (!effectStack.includes(effect)) {
      cleanup(effect)
      try {
        enableTracking()
        effectStack.push(effect)
        activeEffect = effect
        return fn()
      } finally {
        effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  } as ReactiveEffect
  effect.id = uid++
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}

let shouldTrack = true
const trackStack: boolean[] = []

export function pauseTracking() {
  trackStack.push(shouldTrack)
  shouldTrack = false
}

export function enableTracking() {
  trackStack.push(shouldTrack)
  shouldTrack = true
}

export function resetTracking() {
  const last = trackStack.pop()
  shouldTrack = last === undefined ? true : last
}

如上,就是effect部分的源码。顺着执行顺序一步步走下来。

  • 调用方调用effect函数,参数为函数fn,options(默认为{});
  • 判断是否已经是effect过的函数,如果是的话,则直接把原函数返回。
  • 调用createReactiveEffect生成当前fn对应的effect函数,把上面的参数fn和options直接传进去;
  • 判断options里面的lazy是否是false,如果不是懒处理,就直接调用下对应的effect函数;
  • 返回生成的effect函数。

接下来看下createReactiveEffect函数的调用过程。

  • 为effect函数赋值,暂时先不考虑reactiveEffect函数内部到底干了什么,只要明白创建了个函数,并赋值给了effect变量。
  • 然后为effect函数添加属性:id, _isEffect, active, raw, deps, options
  • 把effect返回了。

下面我们回到上面非lazy情况下,调用effect,此时就会执行reactiveEffect函数。

  • 首先判断了是否是active状态,如果不是,说明当前effect函数已经处于失效状态,直接返回return options.scheduler ? undefined : fn()
  • 查看调用栈effectStack里面是否有当前effect,如果无当前effect,接着执行下面的代码。
  • 先调用cleanup,把当前所有依赖此effect的全部清掉,deps是个数组,元素为Set,Set里面放的则是ReactiveEffect,也就是effect;
  • 把当前effect入栈,并将当前effect置为当前活跃effect->activeEffect;后执行fn函数;
  • finally,把effect出栈,执行完成了,把activeEffect还原到之前的状态;
  • 其中涉及到调用轨迹栈的记录。和shouldTrack是否需要跟踪轨迹的处理。

stop

stop方法是用来停止当前effect的。属于Vue3内部方法,来看下源码:

export function stop(effect: ReactiveEffect) {
  if (effect.active) {
    cleanup(effect)
    if (effect.options.onStop) {
      effect.options.onStop()
    }
    effect.active = false
  }
}
  • 调用cleanup清空掉,和上面调用cleanup一样。
  • 执行当前effect.options.onnStop钩子函数。
  • 把当前effect的active状态置为false。

结言

hello,大家好,我是德莱问。

本篇文章主要围绕reactivity文件夹里面提供给大家使用的compositionApi的部分进行了相对应的使用和源码解读,大家感兴趣的还是去读下这部分的源码,毕竟这是Vue3新出的功能,越来越react的一步......

欢迎大家一起来讨论Vue3,刚出的版本,带来了新的同时,肯定也会带着意想不到的惊喜(bug),让我们发现它,解决掉它,也是一种进步,也是防止自己踩坑的好方法。

基于业务沉淀组件 manage-table

2020年下半年,有几张图片刷屏:有人骑在自行车上看书,有人边骑车边用电脑,有人床上铺满了一摞摞书……“边骑车边用电脑”的同学被称为“卷王”登上热搜。
慢慢的这些同学毕业了,卷王带着卷走上了社会,带动了其他人一起卷,卷的人越来越多了,苦不堪言,就导致了一些重复造轮子和造一些毫无意义的轮子的现象出现。

造轮子,本来是件好事,但是随着内卷的出现,造轮子就慢慢演变成了一个极端,出现了凭空造轮子和重复造轮子的事情,既不能服务于业务,还使得内卷现象越来越严重,真正的苦不堪言。

分析当前业务遇到的问题,进而产生新的思路和总结,利用技术的手段提升工作效率,提高开发速度,才是真正的有意义的轮子,也不枉一场。

场景

CMS(content management system)一词出现已久,通常指的是内容管理系统,是一种位于WEB前端和后端办公系统或流程之间的软件系统。在开发cms后台的过程中,最最常用的应该就是Table了,例如 antd的table:

图片.png
这应该是最最常用的开发后台管理系统中使用到的组件了,没有个Table,都不好意思说是个cms系统。不过在稍微庞大的业务中会存在一个非常常见的问题,就是一个数据源会有很多很多字段需要进行展示,如果都展示出来呢,就会存在一个非常不美观且乱糟糟的感觉,眼花缭乱。同时不同的人,希望看到的字段也是不一样的,比如A同学希望看到标题0、1、2、3,B同学希望看到标题1、2、3、4,C同学希望看到标题7、8、9、10等。

这样就是一个非常个性化的需求了,如果希望后端同学来参与的话,就会增加后端同学的工作量,同时前端工作也不会相应的减少。得益于浏览器的localstorage存储能力,前端就可以实现,根本不需要后端同学的参与。

实现

首先,既然是antd的Table组件,我们肯定是要基于现有的功能去实现这个需求,所以我们需要在Table组件的基础上套一层,既不能影响Table的展示,同时还能够定制展示列。那我们就可以列一下需求了:

  1. 不影响Table的展示
  2. 可以选择自定义展示列
  3. 可以对展示列进行排序
  4. 不会对业务产生其他影响(这是最主要的)

需求既然已经明确,我们就可以开整了,具体的实现,就不多说了,我们可以看下实现后的效果:

图片.png

打磨

既然已经实现了最初的需求,就可以高枕无忧了。怎么可能呢?想太多了吧!!!

是的,后来产品说,现在数据展示列太多了,比之前多了三倍,想在对展示列进行选择的时候进行一下分组,不然都挤在一块密密麻麻的不好找,严重影响工作效率了

WTF!最见不得别人说影响工作效率了,这么严重的问题怎么现在才说,怎么不早点提需求过来呢?早点提过来肯定早就实现了啊,不会存在影响工作效率的问题啊.

啊!!!我可真是个口是心非的渣男,可是我知道,小蝌蚪才是渣男,我不配啊!!说多了都是泪啊,还是抓紧做需求吧。看下实现效果:

图片.png
嗯,完美,就是这么个效果。对Table的封装进行了二次修改,在不影响之前的使用方式的基础上,增加了对分组的能力支持,我可真TM棒!

> 然而,快乐的时光总是那么短暂啊~~

有一天,我们的另外一个平台发现,咦,你这个功能还怪好用嘞,能不能给我们也用用,好吧,最简单直接的方式是复制粘贴呀。复制粘贴到一半的时候,突然又来了一个人也想用用这个功能,WTMD就很头大。

这么说来,还是封装成一个npm包吧,等我会,我给你们发布成一个组件包,你们直接安装使用即可。

npm i manage-table

尽管拿去用吧。

使用

安装

npm i manage-table
or
yarn add manage-table

manage-table组件有对应的peerDependencies,如果没有安装的话,需要手动安装一下对应的依赖:

"peerDependencies": {
  "@ant-design/icons": "^4.6.4",
  "antd": "^4.12.0",
  "react": "^17.0.0",
  "react-beautiful-dnd": "^13.1.0"
}

使用方式-: 直接引用,使用内置设置

代码如下:

import ManageTable  from "manage-table";
import './App.css';
import React from "react";

function App() {
  const mockColumns = new Array(50).fill('').map((_item: string, index) => {
    return {
      dataIndex: 'title' + index,
      key: 'title' + index,
      title: '标题' + index,
      show: index % 3 === 0,
    };
  });
  mockColumns.push({
    dataIndex: 'action',
    key: 'action',
    title: '操作',
    show: true,
  });
  return (
    <div className="App">
      <ManageTable name="testTable" columns={mockColumns}/>
    </div>
  );
}

export default App;

效果如下:

图片.png

使用方式二:自定义header部分

代码如下:

import React from "react";
import { Button } from "antd";
import ManageTable from "manage-table";

export default function App2() {
  const mockColumns = new Array(50).fill("").map((_item, index) => {
    return {
      dataIndex: "title" + index,
      key: "title" + index,
      title: "标题" + index,
      show: index % 3 === 0
    };
  });
  mockColumns.push({
    dataIndex: "action",
    key: "action",
    title: "操作",
    show: true
  });
  const ref = React.createRef();
  const handleShowModal = () => {
    ref.current.showModal();
  };
  const SettingHeader = (
    <div style={{ textAlign: "left" }}>
      <Button onClick={handleShowModal}>自定义设置</Button>
    </div>
  );
  return (
    <div className="App">
      <ManageTable
        ref={ref}
        SettingComp={SettingHeader}
        name="testTable2"
        columns={mockColumns}
      />
    </div>
  );
}

效果如下:

图片.png

使用方式三:分组展示

代码如下:

import React from "react";
import { Button } from "antd";
import ManageTable from "manage-table";

const mockGroup = () => {
  const data = new Array(4).fill('').map((_item:string, index: number) => {
    return {
      title: '分组' + index,
      records: new Array(10).fill('').map((_item: string, indx) => {
        return {
          dataIndex: 'title' + index + '_' + indx,
          key: 'title' + index + '_' + indx,
          title: '标题' + index + '_' + indx,
          show: indx % 5 === 0,
        };
      }),
    };
  });
  // 任何一个索引都可以,不必须是0
  data[0].records.push({
    dataIndex: 'action',
    key: 'action',
    title: '操作列',
    show: true,
  })
  return data;
}

export default function AppGroupRef() {
  const ref: any = React.createRef();

  const handleSet = () => {
    ref.current.showModal();
  }

  const SettingHeader = (
    <div style={{textAlign: 'left'}}>
      <Button type="primary" onClick={handleSet}>自定义设置</Button>
    </div>
  );
  return (
    <div className="App">
      <ManageTable ref={ref} SettingComp={SettingHeader} name="testTableGroup" columns={mockGroup()}/>
    </div>
  );
}

效果如下:

图片.png

其他方式

除了可以上面三种方式使用之外,还支持固定展示的配置,即部分字段默认展示且不允许进行排序和删除。manage-table默认是存储在浏览器的缓存里面的,是跟随浏览器走的,如果不想走浏览器缓存,而是自定义存储的话,也是支持的。

具体如下:

ManageTable, 继承自antd的Table

参数名 类型 说明
name string 存储所使用的唯一的key,必传
columns ManageColumnType[] GroupManageColumn[]
ref React.createRef()的返回对象 增加面板, 非必传
SettingComp React.ReactNode 自定义设置头部, 非必传
setTitle React.ReactNode、string 自定义弹窗的标题,默认'设置显示字段', 非必传
defaultShowKeys string[] 默认显示的字段,不需要进行选择or 排序
initialShowKeys string[] 初始显示的字段,自定义存储
onKeysSelected (keys: string[]) => void 存储钩子函数,搭配自定义存储使用

ManageColumnType, 继承自antd的Table的ColumnType

参数名 类型 说明
show boolean 是否默认显示

GroupManageColumn, 继承自antd的Table的ColumnType

参数名 类型 说明
title string 组名,必传
records ManageColumnType[] 列数据, 必传

写在最后

欢迎使用和提交反馈。

2022年了,让我们接着起来吧,停止瞎卷,开启优卷

马上放假了,提前祝大家新年快乐吧~

给大家推荐一款Vue组件切换动画库

问个好

hello,大家好,我是德莱问,又和大家见面了,年后开工在即,新的一年新的气象,祝大家新年快乐,牛年大吉。

欢迎关注我的github,文章的更新和发布第一时间会在github进行

这是博客地址,欢迎+star

来个介绍

安装

npm install transx
or
yarn add transx

使用

<!-- 包裹动画元素 -->
<trans-x
  :time="time"
  :delay="delay"
  :autoplay="autoplay"
  :loop="loop"
  :nextTransition="nextTransition"
  :prevTransition="prevTransition"
  ref="transx"
  @over="over"
  @transitionend="transitionEnd"
>
  <div class="comp" v-for="(item, index) in items" :key="index" :index="index + 1"></div>
</trans-x>
import TransX from "transx";

export default {
  components: {
    TransX
  },
  data() {
    return {
      time: 0.6,
      loop: true,
      autoplay: 1000,
      delay: -1,
      nextTransition: "fadeIn",
      prevTransition: "fadeIn",
      defaultIndex: 0
    }
  }
}

支持参数

参数 说明 类型 默认值 备注
time 动画时长 number 0.6 单位秒
loop 是否循环展现 boolean true
autoplay 是否自动循环 boolean, number false autoplay传递为true时,会赋予默认值1000,单位毫秒
delay 动画间隔 number -1
defaultIndex 默认显示第几张 number 0
nextTransition 下一个的动画,动画种类见下方 string moveLeftBack
prevTransition 上一个的动画,动画种类见下方 string moveRightBack

支持事件

  • over - 跳转到了边界后的回调,当在第一页继续上翻和在最后一页继续下翻时调用
  over: function(isEnd) {
    console.log('边界到了', isEnd);
  }

** 说明:当边界为翻到第一页时isEnd为false,当边界为翻到最后一页时isEnd为true,

  • transitionend - 动画结束时的回调,在动画结束后调用,参数为当前的索引,值从0开始
  transitionEnd: function(currentIndex) {
    console.log("当前到第几页了: ", currentIndex);
  }

支持API

  • goto - 跳转到第几页,参数为页码标识,索引从0开始
    this.$refs.transx.goto(3); // 跳转到第四页
  • prev - 跳转到上一页
    // 不传参
    this.$refs.transx.prev();
    // 传参
    this.$refs.transx.prev({
        time: 0.6,
        delay: -1,
        transition: "moveLeftQuart", // 参考下面[支持动画种类]
    });
  • next - 跳转到下一页
    // 不传参
    this.$refs.transx.next();
    // 传参
    this.$refs.transx.next({
        time: 0.6,
        delay: -1,
        transition: "moveLeftQuart", // 参考下面[支持动画种类]
    });

支持的动画类型

目前共支持24种动画类型,具体选择哪种动画类型,可以参考例子codesanbox,多试试,说不定有意外惊喜哦。下面放几个例子给大家看看:

  • fadeIn

  • flip

  • shuttleRight

  • zoomRotateIn

说明

  • 目前只支持Vue2,后续会升级支持Vue3,
  • 在使用过程中如果遇到什么问题,可以随时提交issue,issue直达

the last

谢谢大家阅读,再次祝大家新年快乐,牛年大吉

如何正确debug 源码react

网上找了好多debug react的文章,在17上都不好使;在其他前辈们的基础上,进行了自己的尝试,记录下来,算是个开头,使用的是yarn不是npm哈。

  • 安装create-react-app
  • 创建react项目:create-react-app study-react
  • 进入到项目目录,并暴露webpack等配置文件:cd study-react && yarn eject
  • 执行yarn start,确保现在的项目是可以run起来的,结果TMD报错:
     Cannot find module '@babel/plugin-syntax-jsx'
    
  • 没关系,我们安装依赖:yarn add -D @babel/plugin-syntax-jsx
  • 重新执行yarn start,这次跑起来了,打开浏览器输入:http://localhost:3000/,可以看到如下界面:
  • 接下来我们需要把对应的react等依赖,指向源码了,没有源码,我们clone源码,
  • 进入到src目录,执行:git submodule add [email protected]:facebook/react.git,然后切换分支:cd react && git checkout tags/v17.0.0 -b v17.0.0
  • 接下来,我们就得改引用了,也就是webpack的alias配置,进入到项目目录,直接把原先的alias配置全部替换成下面的:
    // @file study-react/config/webpack.config.js
    alias: {
      'react': path.resolve(__dirname, '../src/react/packages/react'),
      'react-dom': path.resolve(__dirname, '../src/react/packages/react-dom'),
      'shared': path.resolve(__dirname, '../src/react/packages/shared'),
      'react-reconciler': path.resolve(__dirname, '../src/react/packages/react-reconciler'),
      'scheduler': path.resolve(__dirname, "../src/react/packages/scheduler"),
    },
    
  • 重新执行yarn start,编译报错:
    ./src/react/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
    Attempted import error: 'afterActiveInstanceBlur' is not exported from './ReactFiberHostConfig'.
    
  • 我们进入到ReactFiberHostConfig文件,修改下导出:
    // @file src/react/packages/react-reconciler/src/ReactFiberHostConfig.js
    // 当前文件下其他内容全部注释掉即可,没用
    export * from './forks/ReactFiberHostConfig.dom'
    
  • 此时编译会继续报错:
    ./src/index.js
    Attempted import error: 'react' does not contain a default export (imported as 'React').
    
  • 是react引用的问题,我们进到/src/index文件,把对react和react-dom的引用改成下面样子:
    import * as React from 'react';
    import * as ReactDOM from 'react-dom';
    
  • 接下来会继续报编译的错:
    ./src/react/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
    Attempted import error: 'unstable_flushAllWithoutAsserting' is not exported from 'scheduler' (imported as 'Scheduler').
    
  • 我们找到unstable_flushAllWithoutAsserting的声明是在SchedulerHostConfig.mock.js里面,所以我们在scheduler增加以下导出,挺多的,别漏了:
    // @file src/react/packages/scheduler/src/Scheduler.js
    export * from './src/SchedulerHostConfig.js';
    
    // @file src/react/packages/scheduler/src/SchedulerHostConfig.js
    // 添加以下
    export {
        unstable_flushAllWithoutAsserting,
        unstable_flushNumberOfYields,
        unstable_flushExpired,
        unstable_clearYields,
        unstable_flushUntilNextPaint,
        unstable_flushAll,
        unstable_yieldValue,
        unstable_advanceTime
    } from './forks/SchedulerHostConfig.mock.js';
    
    export {
        requestHostCallback,
        requestHostTimeout,
        cancelHostTimeout,
        shouldYieldToHost,
        getCurrentTime,
        forceFrameRate,
        requestPaint
    } from './forks/SchedulerHostConfig.default.js';
    
    
  • 接下来还是会报错如下,可以看到是eslint的问题:
    Failed to load config "fbjs" to extend from.
    Referenced from: /Users/wuhongjie/mywork/study-react/src/react/.eslintrc.js
    
  • 我们接下来把eslint给去掉,在webpack的配置文件里面,把ESLintPlugin从代码里面注释掉。重新执行yarn start;
  • 这时候,编译已经不报错了,打开浏览器可以看到:
  • 从上面可以得知什么呢?PROFILEEXPERIMENTAL 属于变量替换的问题,我们继续改webpack的配置:
    // @file config/env.js
    const stringified = {
      'process.env': Object.keys(raw).reduce((env, key) => {
        return env;
      }, {}),
      "__DEV__": true,
      "__PROFILE__": true,
      "__UMD__": true,
      "__EXPERIMENTAL__": true
    };
    
  • 本以为接下来就ok了,重新执行yarn start后,发现还是报错
    Error: Internal React error: invariant() is meant to be replaced at compile time. There is no runtime version.
    
  • 上面报错函数invariant报错,现在我们去处理invariant函数:
    // @file src/react/packages/shared/invariant.js
    export default function invariant(condition, format, a, b, c, d, e, f) {
      return false; // 加上这个,啥也不管直接返回false
      throw new Error(
        'Internal React error: invariant() is meant to be replaced at compile ' +
          'time. There is no runtime version.',
      );
    }
    
  • 接下来,重新执行yarn start,看浏览器:

    yeah,成功了!!!

从0.1 + 0.2 !== 0.3 聊聊计算机基础

表面工作

在日常的工作和学习中,经常会探测自己的底线,计算机基础好与不好,完全能够决定一个人的代码水平和bug出现率。相信大家对这些知识都学过,只是长时间不用就忘记了,今天带大家来回顾一下。

本着通俗易懂的原则,今天把这个题目讲明白。

我们来聊聊这个非常常规的问题,为什么 0.1 + 0.2 !== 0.3.在正式介绍这个问题之前,需要了解下面几
个前置知识。

  • 计算机二进制的表现形式以及二进制的计算方式?
  • 什么是原码、补码、反码、移码,都是用来做什么的?

差不多这几个就够理解这个常规的 0.1 + 0.2 !== 0.3问题了。

第一个前置知识,二进制

我们知道在日常中,有很多种数据的展现,包括我们日常生活中常规使用的10进制、css中表示颜色的16进制、计算机中进行运算的二进制。

二进制的表现形式

在计算机中的计算都是以二进制的形式进行计算的,也就是全都是0或1来表示数字的,我们拿10进制进行举例,如:

  • 10进制的 1 在计算机中表示为 1
  • 10进制的 2 在计算机中表示为 10
  • 10进制的 8 在计算机中表示为 1000
  • 10进制的 15 在计算机中表示为 1111

二进制的计算方式

对于二进制的计算方式,我们分为两种情况来说,一种是整数的计算,一种为小数的计算。

整数部分的二进制计算

我们先说明10进制如何转化为二进制。10进制转化为二进制的方式称为“除 2 取余法”,即把一个10进制数,一直除以2取其余数位。举两个例子

30 % 2 ········· 0
15 % 2 ········· 1
 7 % 2 ········· 1
 3 % 2 ········· 1
 1 % 2 ········· 1
 0

整数的二进制转换是从下往上读的,所以30的二进制表示即为11110.

100 % 2 ········· 0
 50 % 2 ········· 0
 25 % 2 ········· 1
 12 % 2 ········· 0
  6 % 2 ········· 0
  3 % 2 ········· 1
  1 % 2 ········· 1
  0

整数的二进制转换是从下往上读的,所以100的二进制表示即为1100100.

我还专门写了一个函数来转换这个二进制。

function getBinary(number) {
  const binary = [];
  function execute(bei) {
    if (bei === 0) {
      return ;
    }
    const next = parseInt(bei / 2, 10);
    const yu = bei % 2;
    binary.unshift(yu);
    execute(next);
  }
  execute(number);
  return binary.join('');
}
console.log(getBinary(30)); // 11110
console.log(getBinary(100)); // 1100100

接下来,我们再看看怎么把二进制转换成10进制。通俗点讲就是从右到左用二进制的每个数去乘以2的相应次方并递增。举个例子,拿上面的100举例子吧。100的二进制表示为1100100,我们需要做的是:

1100100
= 1 * 2^6 + 1 * 2^5 + 0 * 2^4 + 0 * 2^3 + 0 * 2^2 + 0 * 2^1 + 0 * 2^0
= 100

简单明了,不用多说,看下实现代码:

function getDecimal(binary) {
  let number = 0;
  for (let i = binary.length - 1; i >= 0; i--) {
    const num = parseInt(binary[binary.length - i - 1]) * Math.pow(2, i);
    number += num;
  }
  return number;
}
console.log(getDecimal('11110')); // 30
console.log(getDecimal('1100100')); // 100

小数部分的二进制计算

小数部分的二进制计算与整数部分的二进制计算不同,十进制的小数转化为二进制的小数的计算方式称为“乘二取整法”,即把一个十进制的小数乘以2然后取其整数部分,直到其小数部分为0为止。看个例子:

0.0625 * 2 = 0.125 ········· 0
 0.125 * 2 = 0.25  ········· 0
  0.25 * 2 = 0.5   ········· 0
   0.5 * 2 = 1.0   ········· 1

且小数部分的读取方向也不一样。小数的二进制转换是从上往下读的,所以0.0625的二进制表示即为0.0001,这个是正好能够除尽的情况,很多情况下是除不尽的,例如题目中的0.1和0.2。写个函数转换下:

function getBinary(number) {
  const binary = [];
  function execute(num) {
    if (num === 0) {
      return ;
    }
    const next = num * 2;
    const zheng = parseInt(next, 10);
    binary.push(zheng);
    execute(next - zheng);
  }
  execute(number);
  return '0.' + binary.join('');
}
console.log(getBinary(0.0625)); // 0.0001

再尝试把二进制的小数转换为十进制的小数,因为上面是乘,所以在这边就是除法了,二进制的除法也是可以表示为负指数幂的乘法的,比如1/2 = 2^-1;我们来看下0.0001怎么转换为0.0625:

0.0001
= 0 * 2^-1 + 0 * 2^-2 + 0 * 2^-3 + 1 * 2^-4
= 0.0625

用函数来实现下这个形式吧。

function getDecimal(binary) {
  let number = 0;
  let small = binary.slice(2);
  for (let i = 0; i < small.length; i++) {
    const num = parseInt(small[i]) * Math.pow(2, 0 - i - 1);
    number += num;
  }
  return number;
}
console.log(getDecimal('0.0001')); // 0.0625

二进制转换这一部分我们就先了解到这里,对于 0.1 + 0.2 !== 0.3这个问题,上面的二进制部分,基本是足够了。当然代码部分仅作参考,边界等问题没有做处理...

做个题巩固一下:

18.625 的二进制表示是什么 ??? => 点击查看详情
    18的二进制表示为: 100010
    0.625的二进制表示为: 0.101
    所以18.625的二进制表示为:100010.101
  

第二个前置知识,计算机码

我们知道,计算机中是使用二进制来进行计算的,讲到计算机码,就不得不提 IEEE标准,而涉及到小数部分的运算就不得不提到 IEEE二进位浮点数算术标准的标准编号(IEEE 754)。其标准的二进制表示为

V = (-1)^s * M * 2^E
  • 其中s为符号位,0为正数,1为负数;
  • M为尾数,是一个二进制小数,其中规定第一位只能是1,1和小数点省略
  • E为指数,或者称为阶码

为什么1和小数位要省略呢?因为所有的第一位都为1,省略后可以再末尾再多一位,增加精确度。如果第一位为0的话,那没有任何意义。

一般来说,现在的计算机都支持两种精度的计算浮点格式。一种为单精度(float),一种为双精度(double)。

格式 符号位 尾数 阶码 总位数 偏移值
单精度 1 8 23 32 127
双精度 1 11 52 64 1023

以JavaScript为例,js中使用的是双精度格式来进行计算的,其浮点数是64位。

原码

什么是原码,原码是最简单的,就是符号位加上真值的绝对值, 即用第一位表示符号, 其余位表示值。我们用11位表示如下:

  • +1 = [000 0000 0001]原
  • -1 = [100 0000 0001]原
    因为第一位是符号位,所以其取值区间为[111 1111 1111, 011 1111 1111] = [-1023, 1023];

反码

什么是反码,反码是在原码的基础上进行反转。正数的反是其本身;负数的反码是符号位不变,其余位取反。

  • +1 = [000 0000 0001]原 = [000 0000 0001]反
  • -1 = [100 0000 0001]原 = [111 1111 1110]反

补码

什么是补码,补码是在反码的基础上补位。正数的补码是其本身,负数的补码是在其反码的基础上,再加1.

  • +1 = [000 0000 0001]原 = [000 0000 0001]反 = [000 0000 0001]补

  • -1 = [100 0000 0001]原 = [111 1111 1110]反 = [111 1111 1111]补
    为什么会有补码这玩意呢?

  • 首先在计算机中是没有减法的,都是加法,比如 1 - 1 在计算机中是 1 + (-1).

  • 如果使用原码进行减法运算:

    1 + (-1) = [000 0000 0001]原 + [100 0000 0001]原 
             = [100 0000 0010]原 
             = -2
    

    ===>>> 结论:不对

  • 为解决这个不对的问题于是就有了反码去做减法:

    1 + (-1) = [000 0000 0001]反 + [111 1111 1110]反 
             = [111 1111 1111]反 
             = [100 0000 0000]原 
             = -0
    

    发现值是正确的,只是符号位不对;虽然+0和-0在理解上是一样的,但是0带符号是没有意义的,况且会出现 [000 0000 0000]原 和 [100 0000 0000]原 两种编码方式。

    ===>>> 结论:不大行

  • 为解决上面这个符号引起的问题,就出现了补码去做减法:

    1 + (-1) = [000 0000 0001]补 + [111 1111 1111]补 
             = [000 0000 0000]补 
             = [000 0000 0000]原 
             = 0
    

    这样得到的结果就是完美的了,0用 [000 0000 0000] 表示,不会出现上面 [100 0000 0000]。

    ===>>> 结论:完美

移码

移码,是由补码的符号位取反得到的,一般用做浮点数的阶码,引入的目的是为了保证浮点数的机器零为全0。这个不分正负。

  • +1 = [000 0000 0001]原 = [000 0000 0001]反 = [000 0000 0001]补 = [100 0000 0001]移
  • -1 = [100 0000 0001]原 = [111 1111 1110]反 = [111 1111 1111]补 = [011 1111 1111]移
    细心一点可以发现规律:
  • +1 = [000 0000 0001]原 = [100 0000 0001]移
  • -1 = [100 0000 0001]原 = [011 1111 1111]移

为什么 0.1 + 0.2 !== 0.3 ?

回到我们的题目,我们来看下为什么 0.1 + 0.2 !== 0.3.来看下0.1和0.2的二进制表示。

0.1 = 0.0 0011 0011 0011 0011 0011 0011 0011 0011 0011 .... 0011无限循环
0.2 =  0. 0011 0011 0011 0011 0011 0011 0011 0011 0011 .... 0011无限循环

可以得知0.1和0.2都是一个0011无限循环的二进制小数。

我们由上面知道,JavaScript中的浮点数是64位来进行表示的,那么0.1和0.2是在计算机中又是如何表示的呢?

0.1 = (-1)^ 0 * 1.1 0011 0011 0011 * 2^(-4)
-4 = 10 0000 0100

根据IEEE 754标准可以得知:

V = (-1)^S * M * 2^E
S = 0  // 1位,正数为0,负数为1
E = [100 0000 0100]原 // 11位
  = [111 1111 1011]反 
  = [111 1111 1100]补 
  = [011 1111 1100]移 
M = 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 // 52位(其中1小数点省略)

同理可知0.2的表示:

0.2 = (-1)^ 0 * 1.1 0011 0011 0011 * 2^(-3)
-4 = 100 0000 0011

V = (-1)^S * M * 2^E
S = 0  // 1位,正数为0,负数为1
E = [100 0000 0011]原 // 11位
  = [111 1111 1100]反 
  = [111 1111 1101]补 
  = [011 1111 1101]移
M = 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 // 52位(其中1小数点

两者相加,阶码不相同,我们需要进行对阶操作。

对阶

对阶就会存在尾数移动的情况。

  • 大的阶码向小的阶码看齐,就需要把大的阶码的数的尾数向左移动,此时就有可能在移位过程中把尾数的高位部分移掉,这样就引发了数据的错误。这是不可取的
  • 小的阶码向大的阶码看齐,就需要把小的阶码的数向右移动,高位补0;这样就会把右边的数据给挤掉,这样也就导致了会影响数据的精度,但是不会影响数据的整体大小。

计算机采取的是后者,小看大的办法。这也就是今天这个问题产生的原因,丢失了精度

那么接下来,我们就看看上面的这个移动。

// 0.1
E = [100 0000 0011]原 = [111 1111 1100]反 = [111 1111 1101]补 // 11位, 对阶后
M = 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 // 移动前
M = 0100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 // 移动后
// 0.2 保持不变
E = [100 0000 0011]原 = [111 1111 1100]反 = [111 1111 1101]补 // 11位,不变
M = 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 // 52位,不变

如上就是二进制中0.1和0.2的对阶后的结果,我们对这个数字进行运算比较麻烦,所以我们直接拿0.1和0.2的真值进行计算吧。

真值计算

0.1 = 0.0 0011 0011 0011 0011 0011 0011 0011 0011 0011 .... 0011无限循环
0.2 = 0. 0011 0011 0011 0011 0011 0011 0011 0011 0011 .... 0011无限循环
0.1 + 0.2
    = 0.0001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 (1001...舍弃)
    + 0.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 (0011...舍弃)
    = 0.0100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 (1100...舍弃)
    = 0.2999999999999998

这特么不对啊!!!

我们在浏览器运行的时候得到的值是:

0.1 + 0.2 = 0.30000000000000004

产生上面问题的原因,是在于计算机计算的时候,还会存在舍入的处理
如上面来看,真值计算后的值舍弃的值是1100,在计算机中还会存在舍0入1,即如下:

0.1 + 0.2
    = 0.0001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 (1001...舍弃)
    + 0.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 (0011...舍弃)
    = 0.0100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 (1100...舍弃)
    = 0.0100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1101 (入1)
    = 0.30000000000000004

到此,我们就把这部分聊明白了,如有不对之处,欢迎指出。感谢阅读。

公众号

[德莱问前端] ,欢迎关注,文章首发在公众号上面。

除每日进行社区精选文章收集外,还会不定时分享技术文章干货。

希望可以一起学习,共同进步。

前端面试框架

vue

vue基本

一、 vue的生命周期: beforeCreate、created、beforeMounte、mounted、beforeUpdate、updated、beforeDestory、destroyed;

二、 Vue组件通信:

  • props(emit);
  • $attr和$listeners,
  • 事件bus对象(bus.$on, bus.$emit),
  • provide(inject),
  • v-model(props:value, emit:input ),
  • $children,
  • vuex

三、keep-alive使用及原理,LRU算法

四、vue的v-show和v-if的区别;vue的watch和computed的区别;

五、其他:vue的服务端渲染,例如框架nuxt的使用;前端组件库的使用,如element-ui;

vue2与vue3

3是Proxy+Reflect,2是Object.defineProperty;dom-diff的优化;componentApi等

vue-router

  • 实现的模式:hash & history;两者的区别和分析
  • 事件:全局:beforeEach、afterEach;路由:beforeEnter;组件内:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave
  • 实现原理,源码

vuex

  • vuex是什么?vuex官网
  • state、getters、mutations(commit)、actions(dispatch)、module
  • mapState、mapMutations、mapGetters、mapActions;subscribe,subscribeAction
  • 实现原理,源码等

react

一、生命周期

  1.react16之前的生命周期是什么?
  2.react16之后的生命周期是什么?
  3.react16和react16之前的版本有什么区别?
  4.requestAnimationFrame是什么?requestIdleCallback是什么?如何实现requestAnimationFrame
// 实现requestAnimationFrame
var lastTime = 0
window.requestAnimationFrame = function (callback) {
    let now = Date().now;
    let timeCall = Math.max(0, 16 - (lastTime - now));

    let id = setTimeout(function () {
        callback(now + timeCall)
    }, timeCall)
    
    lastTime = now + timeCall;
    return id;
}

二、react-hooks

1.常用的reacthooks都有哪些?
2.使用useEffect模拟componentDidMount和componentDidUpdate
3.useEffect返回的是什么?做什么用的?
4.useEffect和useLayoutEffect的区别是什么?
5、useMemo和useCallback是什么?做什么用的?有什么区别?

三、react-router

1.如何实现路由切换,有几种方式?
2.有哪几个钩子函数?onEnter和routerWillLeave
3.link与a标签的区别什么?

四、redux

1.redux是什么?做什么的?与vuex有什么区别?
2.redux包含哪几块?state,reducers,actions

五、其他:

1.服务端渲染next;
2.组件库antd;
3.PureComponent与Component的区别是什么?
4.react的性能优化有什么建议?
5.封装一个promise的setState
// 使用promise封装setState
function setStateP(state) {
    return new Promise(resolve => {
        this.setState(state, resolve)
    })
}

工具型-webpack

  • 1.webpack是什么?
  • 2.webpack的工作原理是什么?
  • 3.写过plugin吗?怎么实现的?
  • 4.loader是做什么的?loader和plugin的区别什么?
  • 5.关于webpack的优化建议等
    聊聊webpack

前端面试基础

前端基础

浏览器

  • 浏览器的缓存机制:强缓存与协商缓存,以及其区别是什么?
  • 存储相关:localstorage、sessionStorage、cookie等分别是做什么用的,区别是什么?
  • 浏览器的network面板里面的东西,主要是timing下面的时间段代表的都是什么意思?TTFB是什么?
  • 浏览器的performance用过吗,是用来干什么的?
  • 跨域的原理,跨域的实现方式有哪几种?
  • 浏览器环境下的event loop是怎样的?其实也就是宏任务和微任务,可以看下这篇文章

JavaScript

基础数据类型和引用数据类型

  • 基础数据类型:Undefined、Null、Boolean、String、Number、Symbol
  • 引用数据类型:Object、Array、Date、RegExp、Function
  • 此处可能会考察,typeof、instanceof;包括手动实现以下typeof和instanceof
// 实现typeof
function type(obj) {
	return Object.prototype.toString.call(a).slice(8,-1).toLowerCase();
}
// 实现instanceof
function instance(left,right){
    left=left.__proto__
    right=right.prototype
    while(true){
       if(left==null)
       	  return false;
       if(left===right)
          return true;
       left=left.__proto__
    }
}

原型链

理解原型链是做什么的,也就是:实例.proto === 构造函数.prototype

Object.prototype.__proto__ === null // true
Function.prototype.__proto__ === Object.prototype // true
Object.__proto__ === Function.prototype // true

有个比较好的问题,可以思考下:

function F() {}
Object.prototype.b = 2;
F.prototype.a = 1;
var f = new F();
console.log(f.a) // 1
console.log(f.b) // 2
console.log(F.a) // undefined
console.log(F.b) // 2

上面代码,为什么F.a是undefined?

function F() {}
Object.prototype.b = 2;
Function.prototype.a = 1;
var f = new F();
console.log(f.a) // undefined
console.log(f.b) // 2
console.log(F.a) // 1
console.log(F.b) // 2

上面代码,为什么f.a是undefined?

function F() {}
F.prototype.a = 1;
var f1 = new F()
F.prototype = {
    a: 2
}
var f2 = new F()
console.log(f1.a) // 1
console.log(f2.a) // 2

继承

继承的几种方式:

  • 原型链继承:
    function SuperType() {
      this.name = 'Yvette';
      this.colors = ['red', 'blue', 'green'];
    }
    SuperType.prototype.getName = function () {
        return this.name;
    }
    function SubType() {
        this.age = 18;
    }
    SubType.prototype = new SuperType();
    SubType.prototype.constructor = SubType;
    
    let instance1 = new SubType();
    instance1.colors.push('yellow');
    console.log(instance1.getName());
    console.log(instance1.colors); // ['red', 'blue', 'green', 'yellow']
    
    let instance2 = new SubType();
    console.log(instance2.colors); // ['red', 'blue', 'green', 'yellow']
    
    缺点:
    • 通过原型来实现继承时,原型会变成另一个类型的实例,原先的实例属性变成了现在的原型属性,该原型的引用类型属性会被所有的实例共享。(引用类型值被所有实例共享)
    • 在创建子类型的实例时,没有办法在不影响所有对象实例的情况下给超类型的构造函数中传递参数
  • 构造函数继承:
    function SuperType(name) {
        this.name = name;
        this.colors = ['red', 'blue', 'green'];
    }
    function SubType(name) {
        SuperType.call(this, name);
    }
    let instance1 = new SubType('draven');
    instance1.colors.push('yellow');
    console.log(instance1.colors);  // ['red', 'blue', 'green', 'yellow']
    
    let instance2 = new SubType('ben');
    console.log(instance2.colors);  // ['red', 'blue', 'green']
    
    优点:
    • 可以向超类传递参数
    • 解决了原型中包含引用类型值被所有实例共享的问题 缺点:
    • 方法都在构造函数中定义,函数复用无从谈起。
    • 超类型原型中定义的方法对于子类型而言都是不可见的。
  • 组合继承:
    function SuperType(name) {
        this.name = name;
        this.colors = ['red', 'blue', 'green'];
    }
    SuperType.prototype.sayName = function () {
        console.log(this.name);
    }
    function SuberType(name, age) {
        SuperType.call(this, name);
        this.age = age;
    }
    SuberType.prototype = new SuperType()
    SuberType.prototype.constructor = SuberType
    
    let instance1 = new SuberType('draven', 25);
    instance1.colors.push('yellow');
    console.log(instance1.colors); // ['red', 'blue', 'green', 'yellow']
    instance1.sayName(); //draven
    
    let instance2 = new SuberType('ben', 22);
    console.log(instance2.colors);  // ['red', 'blue', 'green']
    instance2.sayName();//ben
    
    缺点:
    • 无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。
      优点:
    • 可以向超类传递参数
    • 每个实例都有自己的属性
    • 实现了函数复用
  • 寄生组合式继承,寄生组合继承是引用类型最理性的继承范式,使用Object.create在组合继承的基础上进行优化:
    function SuperType(name) {
        this.name = name;
        this.colors = ['red', 'blue', 'green'];
    }
    SuperType.prototype.sayName = function () {
        console.log(this.name);
    }
    function SuberType(name, age) {
        SuperType.call(this, name);
        this.age = age;
    }
    SuberType.prototype = Object.create(SuperType.prototype)
    SuberType.prototype.constructor = SuberType
    let instance1 = new SuberType('draven', 25);
    instance1.colors.push('yellow');
    console.log(instance1.colors); //[ 'red', 'blue', 'green', 'yellow' ]
    instance1.sayName(); //draven
    
    let instance2 = new SuberType('ben', 22);
    console.log(instance2.colors); //[ 'red', 'blue', 'green' ]
    instance2.sayName();//ben
    
  • ES6继承:
    class SuperType {
        constructor(age) {
            this.age = age;
        }
    
        getAge() {
            console.log(this.age);
        }
    }
    
    class SubType extends SuperType {
        constructor(age, name) {
            super(age); // 调用父类的constructor(age)
            this.name = name;
        }
    }
    
    let instance = new SubType(18, 'draven');
    instance.getAge(); // 18
    
    • 类的内部所有定义的方法,都是不可枚举的。(ES5原型上的方法默认是可枚举的)

闭包:

  • 柯理化:
// 实现固定参数的curry
function add(a, b, c, d) {
    return a + b + c + d
}

function curry(fn) {
    const length = fn.length
    let params = []
    return function func() {
        params = params.concat([].slice.call(arguments))
        if (params.length === length) {
            const res = fn.apply(null, params);
            params = [];
            return res;
        } else {
            return func;
        }
    }
}

const addCurry = curry(add);
console.log(addCurry(1, 2)(3, 4)); // 10
console.log(addCurry(2)(3)(4)(5)); // 14
// 实现随意参数的柯理化
function add() {
    let params = [].slice.call(arguments);
    function func() {
        params = params.concat([].slice.call(arguments))
        return func;
    }
    func.toString = () => {
        return  params.reduce((a, b) => {
            return a + b;
        }, 0);
    }
    return func;
}

console.log(add(1, 2)(3, 4)); // 10
console.log(add(2)(3)(4)(5)); // 14
  • 防抖和节流:
    函数防抖和节流,都是控制事件触发频率的方法。
// 防抖
export function debounce(func, wait, immediate) {
    let timeout, args, context, timestamp, result;

    let nowTime = Date.now || function () {
        return new Date().getTime();
    };

    const later = function () {
        let last = nowTime() - timestamp;

        if (last < wait && last >= 0) {
            timeout = setTimeout(later, wait - last);
        } else {
            timeout = null;
            if (!immediate) {
                result = func.apply(context, args);
                if (!timeout) context = args = null;
            }
        }
    };

    return function () {
        context = this;
        args = arguments;
        timestamp = nowTime();
        let callNow = immediate && !timeout;
        if (!timeout) timeout = setTimeout(later, wait);
        if (callNow) {
            result = func.apply(context, args);
            context = args = null;
        }

        return result;
    };
};
// 节流
function throttle(fn, threshhold) {
    let timeout
    let start = new Date;
    threshhold = threshhold || 160
    return function () {
        const context = this, args = arguments, curr = new Date() - 0
        clearTimeout(timeout)//总是干掉事件回调
        if (curr - start >= threshhold) {
            fn.apply(context, args)
            start = curr
        } else {
            //让方法在脱离事件后也能执行一次
            timeout = setTimeout(function(){
                fn.apply(context, args)
            }, threshhold);
        }
    }
}

var/let/const

这部分主要考查对let和var的理解,变量提升等。

看下面这个代码的执行结果是什么?

var foo = {n: 1};
var bar = foo;
foo.x = foo = {n: 2};

bar = ?
foo = ?

上面的执行结果是:bar = {n:1,x:{n:2}}; foo={n:2};

a();
var a=3;
function a(){
alert(10)
}
alert(a)
a=6;
a()

上面的执行结果是:10 3 error;最后的error是因为a不是个function;

== 与 ===

隐式转换的步骤: 主要搞明白在强等和双等的时候做了什么事情,也就好理解了。

强等(===)会首先比较两边的类型是否相同,如果不同则直接返回false;如果类型相同的话,则是按照==来判断的,我们来看下==所引起的隐式转换。

双等号引起的隐式转换

一、首先看双等号前后有没有NaN,如果存在NaN,一律返回false。

二、再看双等号前后有没有布尔,有布尔就将布尔转换为数字。(false是0,true是1)

三、接着看双等号前后有没有字符串, 有三种情况:

1、对方是对象,对象使用toString()或者valueOf()进行转换;

2、对方是数字,字符串转数字;(前面已经举例)

3、对方是字符串,直接比较;

4、其他返回false

四、如果是数字,对方是对象,对象取valueOf()或者toString()进行比较, 其他一律返回false

五、null, undefined不会进行类型转换, 但它们俩相等

.toString()方法和.valueOf()方法数值转换

通常情况下我们认为,将一个对象转换为字符串要调用toString()方法,转换为数字要调用valueOf()方法,但是真正应用的时候并没有这么简单,看如下代码实例:

let obj = {
 name: "draven",
 age: 28
}
console.log(obj.toString()); //[object Object]

同理,我们再看valueOf()方法:

let arr = [1, 2, 3];
console.log(arr.valueOf());//[1, 2, 3]

从上面的代码可以看出,valueOf()方法并没有将对象转换为能够反映此对象的一个数字。相反,我们用toString()

let arr = [1, 2, 3];
console.log(arr.toString());//1,2,3

注:很多朋友认为,转换为字符串首先要调用toString()方法, 其实这是错误的认识,我们应该这么理解,调用toString()方法可以转换为字符串,但不一定转换字符串就是首先调用toString()方法。

我们看下下面代码:

let arr = {};
arr.valueOf = function () { return 1; }
arr.toString = function () { return 2; }
console.log(arr == 1);//true

let arr = {};
arr.valueOf = function () { return []; }
arr.toString = function () { return 1; }
console.log(arr == 1);//true

上面代码我们可以看出,转换首先调用的是valueOf(),假如valueOf()不是数值,那就会调用toString进行转换!

let arr = {};
arr.valueOf = function () { return "1"; }
arr.toString = function () { return "2"; }
console.log(arr == "1");//true

假如"1"是字符串,那么它首先调用的还是valueOf()。

let arr = [2];
console.log(arr + "1");//21

上面的例子,调用的是toString();因为arr.toString()之后是2。

转换过程是这样的,首先arr会首先调用valueOf()方法,但是数字的此方法是简单继承而来,并没有重写(当然这个重写不是我们实现),返回值是数组对象本身,并不是一个值类型,所以就转而调用toString()方法,于是就实现了转换为字符串的目的。

说明

大多数对象隐式转换为值类型都是首先尝试调用valueOf()方法。但是Date对象是个例外,此对象的valueOf()和toString()方法都经过精心重写,默认是调用toString()方法,比如使用+运算符,如果在其他算数运算环境中,则会转而调用valueOf()方法。

let date = new Date();
console.log(date + "1"); //Sun Apr 17 2014 17:54:48 GMT+0800 (CST)1
console.log(date + 1);//Sun Apr 17 2014 17:54:48 GMT+0800 (CST)1
console.log(date - 1);//1460886888556
console.log(date * 1);//1460886888557

举例巩固提高
下面我们一起来做做下面的题目吧!

let a;
console.dir(0 == false);//true
console.dir(1 == true);//true
console.dir(2 == {valueOf: function(){return 2}});//true

console.dir(a == NaN);//false
console.dir(NaN == NaN);//false

console.dir(8 == undefined);//false
console.dir(1 == undefined);//false
console.dir(2 == {toString: function(){return 2}});//true

console.dir(undefined == null);//true

console.dir(null == 1);//false

console.dir({ toString:function(){ return 1 } , valueOf:function(){ return [] }} == 1);//true

console.dir(1=="1");//true
console.dir(1==="1");//false

[] == 0 // true

上面的都可以理解了吗?最后一行代码结果是true的原因是什么?

es6

这部分考查对es6的掌握熟练度,新增的一些类型,语法,等等。推荐大家看一看阮一峰老师的es6的文章

手写实现

js实现bind

// 实现bind
Function.prototype.myBind = function (context,...args) {
    let self = this;
    let params = args;
    return function (...newArgs) {
        self.call(context, ...params.concat(...newArgs))
    }
}
var a = {
    name: 'this is a'
}

function sayName() {
    console.log(this.name, arguments)
}

let newfn = sayName.myBind(a, '1234', '5678')
newfn('1000', '2000')

js实现call

// 实现call
Function.prototype.myCall = function (context,...args) {
    context.fn = this;
    context.fn(...args)
    delete context.fn;
}
var a = {
    name: 'this is a'
}
function sayName() {
    console.log(this.name, arguments)
}
sayName.myCall(a, '1234', '5678')

js实现setInterval

// setTimeout 实现setInterval
function mySetInterval(fn, time) {
    let timer = {};
    function timeout() {
        timer.t = setTimeout(() => {
            fn();
            timeout()
        }, time)
    }
    timeout();
    return timer;
}


function clearMyInterval(timer) {
    clearTimeout(timer.t)
}

promise

promise考察点比较多,包括实现自己的promise和一些调用的知识点

推荐两篇文章:实现PromisePromise题

css

  • 盒模型,盒模型的margin、padding有什么特点?
  • flex布局的属性都有什么,都代表什么含义?
  • 左右居中布局、上下居中布局、上下左右居中布局,实现方式是什么?
  • 单行超出省略...,多行超出省略...
  • 自适应布局
  • 响应式布局
  • less、scss、stylus
  • rem、em、vw等
  • 移动端1px如何实现?
  • css如何实现三角形?
  • css的link和import区别是什么?

html

  • meta用来干嘛的
  • 块元素、行元素区别和举例
  • html5新增的标签有哪些?
  • video标签的使用,事件等

【wp2vite]一个让webpack项目支持vite的命令行工具

wp2vite logo

wp2vite

hello大家好,我是德莱问,
首先介绍一下今天的主角,这是一个命令行工具、自动化工具。

工具的作用是一键让使用webpack来进行开发和构建的项目支持使用vite来进行开发和构建。

如果有人不知道webpackvite 分别是什么,可以点击相对应的名字去到它们的官网瞅瞅。

不过对于一个前端er来说,默认你们是知道webpack的;如果你不知道vite的话,建议了解一下,号称是下一代前端开发与构建工具.

前段时间写过一篇vite解析和尝试的一篇文章 ,在文章最后,简单说明了一下:"vite,真香"。

安装与使用

关于wp2vite的安装,与其他命令行工具安装是一样的:

npm install -g wp2vite
or
yarn global add wp2vite

使用的话,其实是非常简单的,一个特别特别简单的工具,没有那么多配置文件,也没有那么长的命令行;

// 进到你的使用webpack开发和构建的项目的目录
cd your_workspace/your_project
// 执行wp2vite的命令行
wp2vite 
or 
wp2vite init

待wp2vite命令执行完后,进行安装依赖和启动项目

// 安装依赖
npm install

// 启动项目
npm run dev // 如果原先你的项目有dev script,请执行下面的命令
or
npm run vite-start

关于实现

实现这个命令行工具的初衷,其实还是源于vite-content-pro ,将一个webpack的项目concent-pro 改成一个支持vite的项目过程中,费时费力;

作为一个有追求的程序员来说,能够一个命令行搞定的事情,决不手动去复制粘贴~

开整,获取webpack配置

我们根据项目的不同,webpack的配置也是不一样的,对于react项目来说,其实是有两种,得益于社区造轮子的能力丰富:

  • create-react-app: 对于已经进行了eject的create-react-app创建的项目,配置文件是暴露出来的,是在config/webpack.config.js;
  • create-react-app: 对于没有进行eject的create-react-app,配置文件是在node_modules/react-scripts/config/webpack.config.js;
  • react-app-rewired:在准备工作的时候发现有不少项目使用这个进行创建的项目,这个得配置文件是有一个/config-overrides.js的配置文件;

上面这些是对于规范框架创建的react的项目的;对于vue来说,得益于vue全家桶的普及,基本只有一种:

  • vue-cli: 使用它创建的项目,其webpack的配置文件是在/node_modules/@vue/cli-service/webpack.config.js

对于其他类型的webpack项目,暂时没有进行细致的划分,不过这部分也是可以支持的,须要传递一下webpack的配置文件所在的位置。

获取代理-proxy

对于react项目来说,大部分代理都是存放于setupProxy.js里面的,wp2vite对于这种代理进行了处理,会把这里配置的代理直接进行复制到vite的proxy里面;

我们使用nodejs的require.cache获取了这部分的代理,当然了这里面也遇到了不少的坑。

对于vue项目,比较严肃的说,它其实是在vue.config.js里面的,比较容易进行读取。

获取别名-alias

关于alias部分,有一部分别名是在webpack的配置文件里面的,还有一部分其实是在tsconfig.json/jsconfig.json里面的;
我们会对这两部分的数据进行合并,总结出vite的alias。

补充插件-plugin

vite官方其实提供的插件是比较少的,不过造轮子的人是真多,还是有不少插件的,当然跟webpack不能比,它活得久啊!

对于react项目,以js结尾的jsx语法的文件,vite是不会是有jsx-loader进行解析的,我们提供了vite-plugin-react-js-support 补充这部分的不足;
另外我们还会自动注入官方的react-refresh 插件

对于vue项目,我们只会注入一个@vitejs/plugin-vue ; 此插件依赖一个Vue单文件组件@vue/compiler-sfc,会自动加入到依赖中。

对于所有的项目,我们会注入兼容模式的插件@vitejs/plugin-legacy ,并且会给出基本的配置,以便于低端浏览器的兼容处理。

自给自足

其实还有其他的部分工作,就不一一赘述了。

转换工具做的事情,有不少,不过对于不同的项目可能会存在不同的问题,记住一句话 alias是vite的大法,凡是找不到的或者找错了依赖的地方,加alias就对了

还在路上的事情

  • babel7以下的项目转化完成是有点问题的,还在优化;
  • 目前只支持react和vue项目的转换,react表现较好;其他项目暂时是不支持的,正在路上;
  • 测试转换的项目目前仅有10多个,不过都成功了,还需继续考验;

欢迎体验wp2vite,使用过程中有任何问题欢迎联系我们;当然如果你想参与贡献,我们也非常欢迎

Vue2核心之Observe源码分析

正文

入口observe

Observe对外只暴露了一个函数observe,Observer类虽然给了export,但是外部并无调用。

function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (shouldObserve && !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) && !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

上面就是observe函数的源代码;

  • 首先检测了传进来的value,不是对象或者是VNode(虚拟dom)对象,则直接return;
  • 然后判断了一下,当前value是否是已经进行了Observe处理的对象;
  • 上面步骤为false时,进行判断是否需要进行监听,并且不是服务端渲染,并且是可监听对象,可扩展对象,不是Vue对象,则对value进行Observer初始化;
  • vmCount是ob的一个属性,初始值为0,当asRootData为true且ob不为空的时候,vmCount + 1;
  • 返回ob对象;
    此函数作为监听的入口文件,对数据进行拦截判断,返回一个Observer的实例。

类Observer

Observer是一个class,有三个私有属性:value、dep、vmCount;一起来看下其构造函数:

constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

构造函数干了四件事情:

  • 赋值,为私有变量赋值,value为传进来的参数value;dep为Dep的实例,后面讲Dep;vmCount默认值为0;
  • 定义__ob__,为当前value定义__ob__属性,此属性指向Observer的当前实例-this;
  • 如果value为数组,支持__proto__属性则执行protoAugment,否则执行copyAugment;最后调用observeArray;
  • 不为数组,则执行walk;

上面讲到的【支持__proto__属性】,现在主流浏览器都支持的,除了IE;

数组处理

protoAugment和copyAugment方法的参数,参数差别就是最后一个参数arrayKeys;

  • 第一个参数为当前value;
  • 第二个参数为arrayMethods
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
methodsToPatch.forEach(function (method) {
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})

上面会对arrayMethods里面的值进行重新定义,也就是重写会引起数组变化的方法,以达到对数组进行监听的目的。

  • 第三个参数,其实也就是当前浏览器所支持的所有的数组的方法。
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

接下来,咱们分别看下两个方法(protoAugment和copyAugment)的实现:

function protoAugment (target, src: Object) {
  target.__proto__ = src
}
function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}
  • protoAugment方法直接把数组的__proto__指向了src,这样通过array调用arrayMethods里面的方法的时候,就是调用的重写后的方法,也就达到了对数组进行监听的目的;
    copyAugment方法,因为不支持__proto__的缘故,则需要在数组上面覆盖原生的arrayMethods里面的方法,也就达到了对数组进行监听的目的。

数组走observeArray

observeArray非常简单:

observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }

循环调用上面文章开头介绍的observe方法;最终都会走到下面要说的walk方法。

非数组走walk

walk函数也是非常简单:

walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

获取到当前对象的keys后,对keys进行遍历,遍历调用defineReactive,传递两个参数,参数1为当前对象,参数2为当前遍历到的key。

defineReactive

接下来是重头戏,这才是Vue真正的核心之一。来看下defineReactive的源码:

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      .....
    },
    set: function reactiveSetter (newVal) {
      ......
    }
  })
}

先说下调用defineReactive的地方:

  • initInjections,对依赖进行处理的时候,会对inject的key进行响应化调用;
  • initRender,对$attrs和$listeners对象进行浅式响应化调用;
  • initState里面的initProps,会对props进行响应化调用;
  • 上面说到的walk里面会调用;
  • set函数里面会调用,包括Vue.set和原型对象上的$set里面;

接着说下defineReactive函数的参数:

  • 第一个就是要进行响应式处理的对象,obj;
  • 第二个为当前对象下面的一个属性,key;
  • 第三个为默认值,val;
  • 第四个为customSetter,是一个函数,用户设置的set函数时的回调,不过这个函数只有非线上环境才会调用;
  • 第五个参数为是否是浅式响应化,如果是浅式则不会对子对象进行监听。

不用看着defineReactive很长,其实就干了三件事情,一一来看
defineReactive源码解读:

  • 首先声明了一个dep,后面研究Dep是干啥的;
  • 接着判断了当前对象的当前属性是否是可改变,不可改变,直接返回;其实个人觉得,上面的dep声明,,可以放到这后面来,有点浪费;
  • 判断了下是否是无getter或者只有setter,且只有两个参数的时候,会把默认值val设置为obj[key];
  • 最后调用Object.defineProperty,重新声明obj对key的处理方式。

然后咱们接下来看下重新声明后get的定义:

function reactiveGetter () {
  const value = getter ? getter.call(obj) : val
  if (Dep.target) {
    dep.depend()
    if (childOb) {
      childOb.dep.depend()
      if (Array.isArray(value)) {
        dependArray(value)
      }
    }
  }
  return value
}

上面代码是重新定义的get方法,先是调用原生getter方法获取到value,然后判断是否有Dep.target,Dep.target是一个Watcher对象,然后调用收集依赖的函数dep.depend(),然后依次判断childOb,收集依赖;每次调用defineReactive,都有一个唯一的Dep实例与当前value一一对应。

function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }

上面代码是重新定义的set方法,先是调用原生getter方法获取到value,然后对newVal进行判断,如果未发生变化则直接返回;
如果只有getter没有setter则直接返回;然后调用原生setter进行赋值,后面调用dep.notify进行通知更新;notify会调用dep对象下面所有的依赖watcher对象下面的update方法进行更新操作;下面咱们会讲到Dep。

Dep

上面讲到了Dep的使用,现在咱们来看下Dep的实现。

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
  notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

上面代码就是去除生产环境后的代码。

可以看到Dep对象有三个变量:一个是target,target对象是一个Watcher对象,同时是一个static的对象;id为一个数字的变量;subs则为一个Watcher对象的数组;

  • 构造函数为id和subs赋值;
  • addSub为依赖收集的函数;
  • removeSub为删除依赖的函数;
  • depend为依赖收集的函数,此处会调用Watcher实例的addDep函数(会有去重操作);
  • notify函数则是通知函数,此处会循环所有的依赖(Watcher实例),然后调用实例的update方法。
    额外要讲的是,除了Dep的声明外,还有Dep.target这个static类型的变量的处理:
Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

此处会对Dep.target进行赋值等操作,pushTarget和popTarget是成对出现的,有一个pushTarget则必然有一个popTarget;target则依旧是一个Watcher;暴露给外部调用,收集Watcher所使用,下次咱们会讲到Watcher。

结言

本章沿着observe函数进行了一步步的探索,从Observer到Dep.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.