Code Monkey home page Code Monkey logo

notes's Introduction

记录一些平常看到的有趣的前端框架、知识。

notes's People

Contributors

sl1673495 avatar

Stargazers

 avatar syean avatar  avatar ipid avatar  avatar  avatar  avatar Zhanghao avatar sunyanzhe avatar Billow avatar  avatar  avatar 柚子 · 姑娘 avatar ZephyrJS avatar SuperRailgun avatar daxiong avatar Sadhu avatar  avatar Lstoryc.Luo avatar donglongting avatar Chenchen Wang avatar

Watchers

James Cloos avatar PaddingMe avatar Colafornia avatar Zhanghao avatar  avatar  avatar Sadhu avatar

notes's Issues

Vue的diff过程学习

首先响应式数据更新后,触发了渲染 Watcher 的回调函数vm._update(vm._render())

这段代码会带着新生成的 vnode去 触发 _update 函数。

然后触发 patch 过程,

对比旧新旧节点是否是相同类型的节点:

1. 不是相同节点:

isSameNode为false的话,直接销毁旧的 vnode,渲染新的 vnode。

2. 是相同节点,要尽可能的做节点的复用。

会调用src/core/vdom/patch.js下的patchVNode方法。

如果新 vnode 是文字 vnode

就直接调用api把节点的直接替换掉文字内容就好,比如在 web 端就是 node.textContext = text

如果新 vnode 不是文字 vnode

我们暂时把 vnode.chilren 也就是vnode的子节点称为 ch

如果有新 ch 而没有旧 ch

说明是新增 ch,直接 addVnodes 添加新子节点。

如果有旧 ch 而没有新 ch

说明是删除 ch,直接 removeVnodes 删除旧子节点

如果新旧 ch 都存在

那么就是我们 diff算法 想要考察的最核心的点了,也就是新旧节点的 diff 过程。

通过

  let oldStartIdx = 0
  let newStartIdx = 0
  let oldEndIdx = oldCh.length - 1
  let oldStartVnode = oldCh[0]
  let oldEndVnode = oldCh[oldEndIdx]
  let newEndIdx = newCh.length - 1

这些变量分别指向旧节点的首尾新节点的首尾

根据这些指针,对新旧节点的进行对比,其中某一项命中了,就直接进入 patch 节点的过程。

  1. 旧首节点和新首节点用 sameNode 对比。

  2. 旧尾节点和新首节点用 sameNode 对比

  3. 旧首节点和新尾节点用 sameNode 对比

  4. 旧尾节点和新尾节点用 sameNode 对比

  5. 如果以上逻辑都匹配不到,再利用key去对比。

然后不停的把匹配到的指针向内部收缩,直到新旧节点有一端的指针相遇(说明这个端的节点都被patch过了)。

在指针相遇以后,还有两种比较特殊的情况:

  1. 有新节点需要加入。
    如果更新完以后,oldStartIdx > oldEndIdx,说明旧节点都被 patch 完了,但是有可能还有新的节点没有被处理到。接着会走 addVnodes 去判断是否要新增子节点。

  2. 有旧节点需要删除。
    如果新节点先patch完了,那么此时会走 newStartIdx > newEndIdx 的逻辑,那么就会走 removeVnodes 的逻辑去删除多余的旧子节点。

removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);

本文的重点就在于,这个

那么判断是否是相同节点的函数是 sameNode

function sameVnode (a, b) {
  return (
    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      )
    )
  )
}

可以看到,判断是否是 sameVnode,传递给节点的 key 是关键。

为什么不要以index作为key?

假设我们有这样的一段代码:

    <div id="app">
      <ul>
        <item
          :key="index"
          v-for="(num, index) in nums"
          :num="num"
          :class="`item${num}`"
        ></item>
      </ul>
      <button @click="change">改变</button>
    </div>
    <script src="./vue.js"></script>
    <script>
      var vm = new Vue({
        name: "parent",
        el: "#app",
        data: {
          nums: [1, 2, 3]
        },
        methods: {
          change() {
            this.nums.reverse();
          }
        },
        components: {
          item: {
            props: ["num"],
            template: `
                    <div>
                       {{num}}
                    </div>
                `,
            name: "child"
          }
        }
      });
    </script>

其实是一个很简单的列表组件,渲染出来 1 2 3 三个数字。我们先以 index 作为key,来跟踪一下它的更新。

我们接下来只关注 item 列表节点的更新,在首次渲染的时候,我们的虚拟节点列表 oldChildren 粗略表示是这样的:

[
  {
    tag: "item",
    key: 0,
    props: {
      num: 1
    }
  },
  {
    tag: "item",
    key: 1,
    props: {
      num: 2
    }
  },
  {
    tag: "item",
    key: 2,
    props: {
      num: 3
    }
  }
];

在我们点击按钮的时候,会对数组做 reverse 的操作。那么我们此时生成的 newChildren 列表是这样的:

[
  {
    tag: "item",
    key: 0,
    props: {
+     num: 3
    }
  },
  {
    tag: "item",
    key: 1,
    props: {
+     num: 2
    }
  },
  {
    tag: "item",
    key: 2,
    props: {
+     num: 1
    }
  }
];

发现什么问题没有?key的顺序没变,传入的值完全变了。这会导致一个什么问题?

本来按照最合理的逻辑来说,旧的第一个vnode 是应该直接完全复用 新的第三个vnode的,因为它们本来就应该是同一个vnode,自然所有的属性都是相同的。

但是在进行子节点的 diff 过程中,会在 旧首节点和新首节点用 sameNode 对比。 这一步命中逻辑,因为现在新旧两次首部节点key 都是 0了,

然后把旧的节点中的第一个 vnode 和 新的节点中的第一个 vnode 进行 patchVnode 操作。

这会发生什么呢?我可以大致给你列一下:
首先,正如我之前的文章props的更新如何触发重渲染?里所说,在进行 patchVnode 的时候,会去检查 props 有没有变更,如果有的话,会通过 _props.num = 3 这样的逻辑去更新这个响应式的值,触发 dep.notify,触发子组件视图的重新渲染等一套很重的逻辑。

然后,还会额外的触发以下几个钩子,假设我们的组件上定义了一些dom的属性或者类名、样式、指令,那么都会被全量的更新。

  1. updateAttrs
  2. updateClass
  3. updateDOMListeners
  4. updateDOMProps
  5. updateStyle
  6. updateDirectives

而这些所有重量级的操作(虚拟dom发明的其中一个目的不就是为了减少真实dom的操作么?),都可以通过直接复用 第三个vnode 来避免,是因为我们偷懒写了 index 作为 key,而导致所有的优化失效了。

为什么不要用随机数作为key?

<item
  :key="Math.random()"
  v-for="(num, index) in nums"
  :num="num"
  :class="`item${num}`"
/>

其实我听过一种说法,既然官方要求一个 唯一的key,是不是可以用 Math.random() 作为 key 来偷懒?这是一个很鸡贼的想法,看看会发生什么吧。

首先 oldVnode 是这样的:

[
  {
    tag: "item",
    key: 0.6330715699108844,
    props: {
      num: 1
    }
  },
  {
    tag: "item",
    key: 0.25104533240710514,
    props: {
      num: 2
    }
  },
  {
    tag: "item",
    key: 0.4114769152411637,
    props: {
      num: 3
    }
  }
];

更新以后是:

[
  {
    tag: "item",
+   key: 0.11046018699748683,
    props: {
+     num: 3
    }
  },
  {
    tag: "item",
+   key: 0.8549799545696619,
    props: {
+     num: 2
    }
  },
  {
    tag: "item",
+   key: 0.18674467938937478,
    props: {
+     num: 1
    }
  }
];

可以看到,key 变成了完全全新的 3 个随机数。

上面说到,diff 子节点的首尾对比如果都没有命中,就会进入 key 的详细对比过程,简单来说,就是利用旧节点的 key -> index 的关系建立一个 map 映射表,然后用新节点的 key 去匹配,如果没找到的话,就会调用 createElm 方法 重新建立 一个新节点。

具体代码在这:

// 建立旧节点的 key -> index 映射表
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);

// 去映射表里找可以复用的 index
idxInOld = findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
// 一定是找不到的,因为新节点的 key 是随机生成的。
if (isUndef(idxInOld)) {
  // 完全通过 vnode 新建一个真实的子节点
  createElm();
}

也就是说,咱们的这个更新过程可以这样描述:
123 -> 前面重新创建三个子组件 -> 321123 -> 删除、销毁后面三个子组件 -> 321

发现问题了吧?这是毁灭性的灾难,创建新的组件和销毁组件的成本你们晓得的伐……本来仅仅是对组件移动位置就可以完成的更新,被我们毁成这样了。

总结

经过这样的一段旅行,diff 这个庞大的过程就结束了。

我们收获了什么?

  1. 用组件唯一的 id(一般由后端返回)作为它的 key,实在没有的情况下,可以在获取到列表的时候通过某种规则为它们创建一个 key,并保证这个 key 在组件整个生命周期中都保持稳定。

  2. 别用 index 作为 key,和没写基本上没区别。

  3. 千万别用随机数作为 key,不然你的老板会被你气死。

compose 的拆解原理

compose 拆解原理

一步步拆解 compose 函数,看看它到底做了什么样的事情,比较脑壳痛。

假设现在是三个高阶组件的组合:

const compsosed = compose(
    withA,
    withB,
    withC,
    withD
)

const hoc = compsosed(view)
  1. 首先在 reduce 的第一次循环里,awithAbwithB,然后 return 了:
(...args) => withA(withB(...args))

这个 return 的值就会作为 reduce 中下次循环的 a

  1. 下一次循环,那么此时的b 是我们假设的另一个高阶组件 withC,那么就 return 了
(...args2) => (...args) => withA(withB(...args))(withC(...args2))
               这里是a                          ↑这里是(b(args))
  1. 下一次循环,那么此时的b 是我们假设的另一个高阶组件 withD,那么就 return 了
(...args3) => (...args2) => (...args) => (withA(withB(...args))(withC(...args2)))(withD(...args3))
  1. 此时我们如果外部传入了 view,上一步中的 args2 就会被消除,这个函数会先归约成这样:
(...args2) =>  (...args) => (withA(withB(...args))(withC(...args2)))(withD(view))

withD(view) 又会作为...args2 进一步执行,归约为:

(...args) => (withA(withB(...args))(withC(withD(view))))

withC(withD(view)) 又会进一步的作为 ...args 归约。

withA(withB(withC(withD(view)))

可以看到,compose 函数不断的把函数高阶包裹,在执行的时候又一层一层的解包,非常巧妙的构思。

Vue的Transition组件学习笔记

transition组件会向下查找第一个真实vnode,也就是说keep-alive这种会被排除,并且在vnode.data.transition中保存了动画相关的值。

然后在src/platforms/web/modules/transition.js这个模块中,定义了动画vnode在enter和destory的时机应该如何添加动画,比如在合适的时机加上动画类名。

const autoCssTransition: (name: string) => Object = cached(name => {
  return {
    enterClass: `${name}-enter`,
    enterToClass: `${name}-enter-to`,
    enterActiveClass: `${name}-enter-active`,
    leaveClass: `${name}-leave`,
    leaveToClass: `${name}-leave-to`,
    leaveActiveClass: `${name}-leave-active`
  }
})

主要的进入动画链路:

if (expectsCSS) {
  addTransitionClass(el, startClass)
  addTransitionClass(el, activeClass)
  nextFrame(() => {
    removeTransitionClass(el, startClass)
    if (!cb.cancelled) {
      addTransitionClass(el, toClass)
      if (!userWantsControl) {
        if (isValidDuration(explicitEnterDuration)) {
          setTimeout(cb, explicitEnterDuration)
        } else {
          whenTransitionEnds(el, type, cb)
        }
      }
    }
  })
}

注意是在nextFrame才加上了toClass,否则会被直接合并到一次渲染中去,这涉及到浏览器的eventLoop。

这里是双层的raf,只有这样才能保证下一帧调用。

const raf = inBrowser
  ? window.requestAnimationFrame
    ? window.requestAnimationFrame.bind(window)
    : setTimeout
  : /* istanbul ignore next */ fn => fn()

export function nextFrame (fn: Function) {
  raf(() => {
    raf(fn)
  })
}

细节1:加上appear参数,可以使得组件在初次挂载时也能有动画。

细节2:抽象组件不会出现在组件的父子关系链中

细节3: whenTransitionEnds对于一个dom节点如何判断动画结束时机,如果transition-duration的值有多个,说明监听了多个动画属性,那么要利用类似于Promise.all的思路去监听全部的动画执行完毕。

// whenTransitionEnds
export function whenTransitionEnds (
  el: Element,
  expectedType: ?string,
  cb: Function
) {
  const { type, timeout, propCount } = getTransitionInfo(el, expectedType)
  if (!type) return cb()
  const event: string = type === TRANSITION ? transitionEndEvent : animationEndEvent
  let ended = 0
  const end = () => {
    el.removeEventListener(event, onEnd)
    cb()
  }
  const onEnd = e => {
    if (e.target === el) {
      if (++ended >= propCount) {
        end()
      }
    }
  }
  setTimeout(() => {
    if (ended < propCount) {
      end()
    }
  }, timeout + 1)
  el.addEventListener(event, onEnd)
}

如何成长为git小能手(一些实用的小技巧)

Vue路由异步加载状态的展示

function lazyLoadView (AsyncView) {
  const AsyncHandler = () => ({
    component: AsyncView,
    // A component to use while the component is loading.
    loading: require('./Loading.vue').default,
    // A fallback component in case the timeout is exceeded
    // when loading the component.
    error: require('./Timeout.vue').default,
    // Delay before showing the loading component.
    // Default: 200 (milliseconds).
    delay: 400,
    // Time before giving up trying to load the component.
    // Default: Infinity (milliseconds).
    timeout: 10000
  })

  return Promise.resolve({
    functional: true,
    render (h, { data, children }) {
      // Transparently pass any props or children
      // to the view component.
      return h(AsyncHandler, data, children)
    }
  })
}
const router = new VueRouter({
  routes: [
    {
      path: '/foo',
      component: () => lazyLoadView(import('./Foo.vue'))
    }
  ]
})

原因在于 vue-router 本身是不支持 loading 占位组件的,所以要先渲染 lazyLoadView 返回的高阶组件,在组件内部再利用 vue 内部对于 loading 的支持去显示占位组件。

vue-router 和 vue 里都有一个方法去加载异步组件,叫做 resolveAsyncComponent ,但是他们的实现是不一致的,vue-router 只是单纯的等待 promiseresolve 之后,跳转路径,渲染组件。

Vue 2.6 组件化过程的一些变动

image

创建子组件实例的过程发生在 _update 过程中。

调用栈大概变成这个样子了,创建子组件实例的过程发生在这个函数中(很容易被忽略)

if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
  return
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.

简单来说如果是子组件,才会有自己的instance,然后上面就会 return。

具体的创建子组件实例的过程还是发生在子组件 vnode 的 init 生命周期里。

    init: function init (vnode, hydrating) {
      if (
        vnode.componentInstance &&
        !vnode.componentInstance._isDestroyed &&
        vnode.data.keepAlive
      ) {
        // kept-alive components, treat as a patch
        var mountedNode = vnode; // work around flow
        componentVNodeHooks.prepatch(mountedNode, mountedNode);
      } else {

        // 这里
        var child = vnode.componentInstance = createComponentInstanceForVnode(
          vnode,
          activeInstance
        );
        child.$mount(hydrating ? vnode.elm : undefined, hydrating);
      }
    },

createComponentInstanceForVnode 里会去真正的用 new 关键字构造出子组件的实例。

  function createComponentInstanceForVnode (
    vnode, // we know it's MountedComponentVNode but flow doesn't
    parent // activeInstance in lifecycle state
  ) {
    var options = {
      _isComponent: true,
      _parentVnode: vnode,
      parent: parent
    };
    // check inline-template render functions
    var inlineTemplate = vnode.data.inlineTemplate;
    if (isDef(inlineTemplate)) {
      options.render = inlineTemplate.render;
      options.staticRenderFns = inlineTemplate.staticRenderFns;
    }
    return new vnode.componentOptions.Ctor(options)
  }

进入了子实例的 this._init()后,又会进入和上层组件一样的逻辑中。

生成完实例后,注意下面会调用子组件自身的 $mount

注意调用堆栈,这个过程发生在父组件的 beforeMountmounted 之间。

use-immer: React中使用immer的简单封装

源码也很简单,就是把用户的update函数交给produce执行后返回给hook的set方法。

import produce, { Draft } from "immer";
import { useState, useReducer, useCallback } from "react";

export type Reducer<S = any, A = any> = (
  draftState: Draft<S>,
  action: A
) => void | S;

export function useImmer<S = any>(
  initialValue: S | (() => S)
): [S, (f: (draft: Draft<S>) => void | S) => void];
export function useImmer(initialValue: any) {
  const [val, updateValue] = useState(initialValue);
  return [
    val,
    useCallback(updater => {
      updateValue(produce(updater));
    }, [])
  ];
}

export function useImmerReducer<S = any, A = any>(
  reducer: Reducer<S, A>,
  initialState: S,
  initialAction?: (initial: any) => S
): [S, React.Dispatch<A>];
export function useImmerReducer(reducer, initialState, initialAction) {
  const cachedReducer = useCallback(produce(reducer), [reducer]);
  return useReducer(cachedReducer, initialState as any, initialAction);
}

webpack大型项目实战,还有一些loader编写教学。

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.