Jokcy

Jokcy 查看完整档案

杭州编辑  |  填写毕业院校承道网络科技  |  前端架构师 编辑 blog.jokcy.me 编辑
编辑

前端码农一枚

个人动态

Jokcy 发布了文章 · 7月21日

vue3响应式数据最全最细致解析,vue3源码解析持续更新中

本文是我新开的坑的第一篇文章,这个坑就是vue3,接下来我会围绕着vue3进行一系列的动作,包括但不限于:

  • 完整的源码解析
  • jsx工程最佳实践
  • 个性化的jsx方案
  • vue3生态库开发(目前有一个正在进行)
  • 以及可能的自定义一个vue3的runtime

关于源码解析,网站已经上线,vue3源码解析,最佳实践,网站是逐行代码形式的解析,更多关注于源码,而在掘金上分享的文章则类似于总结,会用更复合一篇文章的结构来写。如果你想持续跟进vue3源码,可以打开前面的网站关注我。

那么,开始!

vue3最大的变化莫过于其对于响应式原理的重构,以及其新发布的composition api,本文聚焦于前者,来深度剖析一下vue3中响应式到底是怎么实现的。

我们以reactiveAPI 为例,

const Comp = {
    setup() {
        const state = reactive({
            a: 'jokcy'
        })
        
        return () => {
            return <input value={state.a} onChange={(e) => state.a = e.targent.value} />
        }
    }
}

我们看上面的例子,这个例子很简单,创建了一个组件,他有一个响应式的数据对象,然后render里面的input的value绑定的是state.a以及他的onChange会修改state.a。这是非常简单且直观的一个数据绑定的例子,而这个逻辑能实现的根本原因,是我们在调用state.a = xxx的时候,vue会重新渲染我们return的render函数,来更新节点

而篇文章就是要来看一下,我们通过reactive创建的对象,到底有什么魔力,能够帮我们完成这个任务。

其实本身 API 是很简单的,传入一个对象,返回一个 reactive 对象,创建的方法是createReactiveObject

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
  );
}
function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  // 前面都是一些对象是否已经proxy等的判断逻辑,这里就不展示了

  const observed = new Proxy(
    target,
    collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers
  );
  def(
    target,
    isReadonly ? ReactiveFlags.READONLY : ReactiveFlags.REACTIVE,
    observed
  );
  return observed;
}

那么这里最重要的就是new Proxy了,可以说理解 vue3 的响应式原理过程就是理解这个proxy创建的过程,而了解这个过程,主要就是看第二个参数,在这里就是collectionHandlers或者baseHandlers,大部分是后者,前者主要针对,Set、Map 等。

那么我们就来看baseHandlers

export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys,
};

可见 vue 代理了这几个操作,那么我们一个个看这几个操作做了啥:

function get(target: object, key: string | symbol, receiver: object) {
  // ...内部key的货足

  // 关于数组的一些特殊处理
  const targetIsArray = isArray(target);
  if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
    return Reflect.get(arrayInstrumentations, key, receiver);
  }

  // 获取请求值
  const res = Reflect.get(target, key, receiver);

  // ...如果是内部值的获取则直接返回res

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

  // 返回的一些处理

  return res;
}

这里的重点就在于track(target, TrackOpTypes.GET, key);,这个是我们正常获取值的时候都会执行到的代码:

export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (!shouldTrack || activeEffect === undefined) {
    return;
  }
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }
  let dep = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, (dep = new Set()));
  }
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect);
    activeEffect.deps.push(dep);
    if (__DEV__ && activeEffect.options.onTrack) {
      activeEffect.options.onTrack({
        effect: activeEffect,
        target,
        type,
        key,
      });
    }
  }
}

好了,重点来了,我们逐行分析。

第一个if,就是根据环境变量的shouldTrack来判断是否要进行跟踪,如果你已经看过我的源码解析中的processComponent的章节,那么你现在应该就是豁然开朗的感觉。因为在执行processComponent里面的setup的时候,我们特地关闭了track,而那时候就是把shouldTrack改为了false

let depsMap = targetMap.get(target)这行,targetMap是一个 map,用来记录所有的响应对象。之后如果目前没有记录该对象,那么就重新记录。

这个 target 对应的也是一个 map,他会对每个 key 建立一个 set。

最后要记录这次的 effect 了,这里所谓的effect是什么呢?就是当前正在调用这个对象的函数。在我们的例子里面,就是return回去的render函数,这个函数在执行的时候会调用state.a所以会进入proxy对于get的代理,这个时候 proxy 就调用了track,那么这时候的activeEffect就是这个 render 方法。这里的effect就是,当state.a改动的时候,我们需要重新执行该 render 方法来进行渲染。

那么他是什么时候被设置的呢?在mount章节的时候我们提到了,在执行render方法的时候,我们执行这样一句代码instance.update = effect(function componentEffect()...),就是在这里调用的effect方法里面,把activeEffect记录为componentEffect,而这个componentEffect里面则运行了render方法。

另外这个getHandler里面有句代码也挺有意思的:

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

在获取属性的时候,如果返回的值是对象的时候,才会对其执行reactive,这也就是延迟处理,而且readonly的话是不执行reactive的。

OK,到这里我们已经知道了在执行render函数的时候,因为我们调用了state.a所以这个函数就相当于依赖state.a,这在vue3里面被称为effect

那么下一篇文章,我们就来讲一讲,这些effectstate.a变动的时候是如何被调用的,敬请期待。

查看原文

赞 0 收藏 0 评论 0

Jokcy 发布了文章 · 2018-09-25

为什么你应该放弃老的React Context API改用新的Context API

React16.3发布了新的Context API,并且已经确认了将在下一个版本废弃老的Context API。所以大家更新到新的Context API是无可厚非的事情。而这篇文章会从原理的角度为大家分析为什么要用新的API--不仅仅是因为React官方要更新,毕竟更新了你也可以用16版本的React来使用老的API--而是因为新的API性能比老API 高出太多

用法

我们先来看一下两个版本的Context API如何使用

// old version
class Parent extends Component{
  getChildContext() {
    return {type: 123}
  }
}

Parent.childContextType = {
  type: PropTypes.number
}

const Child = (props, context) => (
  <p>{context.type}</p>
)

Child.contextTypes = {
  type: PropTypes.number
}

通过在父组件上声明getChildContext方法为其子孙组件提供context,我们称其ProviderComponent。注意必须要声明Parent.childContextType才会生效,而子组件如果需要使用context,需要显示得声明Child.contextTypes

// new version
const { Provider, Consumer } = React.createContext('defaultValue')

const Parent = (props) => (
  <Provider value={'realValue'}>
    {props.children}
  </Provider>
)

const Child = () => {
  <Consumer>
    {
      (value) => <p>{value}</p>
    }
  </Consumer>
}

新版本的API,React提供了createContext方法,这个方法会返回两个组件ProviderConsumberProvider用来提供context的内容,通过向Provider传递value这个prop,而在需要用到对应context的地方,用相同来源的Consumer来获取contextConsumer有特定的用法,就是他的children必须是一个方法,并且context的值使用参数传递给这个方法。

性能对比

正好前几天React devtool发布了Profiler功能,就用这个新功能来查看一下两个API的新能有什么差距吧,先看一下例子

不知道Profiler的看这里

// old api demo
import React from 'react'
import PropTypes from 'prop-types'

export default class App extends React.Component {
  state = {
    type: 1,
  }

  getChildContext() {
    return {
      type: this.state.type
    }
  }

  componentDidMount() {
    setInterval(() => {
      this.setState({
        type: this.state.type + 1
      })
    }, 500)
  }

  render() {
    return this.props.children
  }
}

App.childContextTypes = {
  type: PropTypes.number
}

export const Comp = (props, context) => {
  const arr = []
  for (let i=0; i<100; i++) {
    arr.push(<p key={i}>{i}</p>)
  }

  return (
    <div>
      <p>{context.type}</p>
      {arr}
    </div>
  )
}

Comp.contextTypes = {
  type: PropTypes.number
}
// new api demo
import React, { Component, createContext } from 'react'

const { Provider, Consumer } = createContext(1)

export default class App extends Component {

  state = {
    type: 1
  }

  componentDidMount() {
    setInterval(() => {
      this.setState({
        type: this.state.type + 1
      })
    }, 500)
  }

  render () {
    return (
      <Provider value={this.state.type}>
        {this.props.children}
      </Provider>
    )
  }

}

export const Comp = () => {
  const arr = []
  for (let i=0; i<100; i++) {
    arr.push(<p key={i}>{i}</p>)
  }

  return (
    <div>
      <Consumer>
        {(type) => <p>{type}</p>}
      </Consumer>
      {arr}
    </div>
  )
}
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

import App, {Comp} from './context/OldApi'

// import App, { Comp } from './context/NewApi'

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

代码基本相同,主要变动就是一个interval,每500毫秒给type加1,然后我们来分别看一下Profiler的截图

不知道Profiler的看这里

老API

老API

新API

新API

可见这两个性能差距是非常大的,老的API需要7点几毫秒,而新的API只需要0.4毫秒,而且新的API只有两个节点重新渲染了,而老的API所有节点都重新渲染了(下面还有很多节点没截图进去,虽然每个可能只有0.1毫秒或者甚至不到,但是积少成多,导致他们的父组件Comp渲染时间很长)

进一步举例

在这里可能有些同学会想,新老API的用法不一样,因为老API的context是作为Comp这个functional Component的参数传入的,所以肯定会影响该组件的所有子元素,所以我在这个基础上修改了例子,把数组从Comp组件中移除,放到一个新的组件Comp2

// Comp2
export class Comp2 extends React.Component {
  render() {
    const arr = []
    for (let i=0; i<100; i++) {
      arr.push(<p key={i}>{i}</p>)
    }

    return arr
  }
}

// new old api Comp
export const Comp = (props, context) => {
  return (
    <div>
      <p>{context.type}</p>
    </div>
  )
}

// new new api Comp
export const Comp = () => {
  return (
    <div>
      <Consumer>
        {(type) => <p>{type}</p>}
      </Consumer>
    </div>
  )
}

现在受context影响的渲染内容新老API都是一样的,只有<p>{type}</p>,我们再来看一下情况

老API

老API

新API

新API

忽视比demo1时间长的问题,应该是我电脑运行时间长性能下降的问题,只需要横向对比新老API就可以了

从这里可以看出来,结果跟Demo1没什么区别,老API中我们的arr仍然都被重新渲染了,导致整体的渲染时间被拉长很多。

事实上,这可能还不是最让你震惊的地方,我们再改一下例子,我们在App中不再修改type,而是新增一个statenum,然后对其进行递增

// App
export default class App extends React.Component {
  state = {
    type: 1,
    num: 1
  }

  getChildContext() {
    return {
      type: this.state.type
    }
  }

  componentDidMount() {
    setInterval(() => {
      this.setState({
        num: this.state.num + 1
      })
    }, 500)
  }

  render() {
    return (
      <div>
        <p>inside update {this.state.num}</p>
        {this.props.children}
      </div>
    )
  }
}
老API

老API

新API

新API

可以看到老API依然没有什么改观,他依然重新渲染所有子节点。

再进一步我给Comp2增加componentDidUpdate生命周期钩子

componentDidUpdate() {
  console.log('update')
}

在使用老API的时候,每次App更新都会打印

而新API则不会

总结

从上面测试的结果大家应该可以看出来结果了,这里简单的讲一下原因,因为要具体分析会很长并且要涉及到源码的很多细节,所以有空再写一片续,来详细得讲解源码,大家有兴趣的可以关注我。

要分析原理要了解React对于每次更新的处理流程,React是一个树结构,要进行更新只能通过某个节点执行setState、forceUpdate等方法,在某一个节点执行了这些方法之后,React会向上搜索直到找到root节点,然后把root节点放到更新队列中,等待更新。

所以React的更新都是从root往下执行的,他会尝试重新构建一个新的树,在这个过程中能复用之前的节点就会复用,而我们现在看到的情况,就是因为复用算法根据不同的情况而得到的不同的结果

我们来看一小段源码

if (
  !hasLegacyContextChanged() &&
  (updateExpirationTime === NoWork ||
    updateExpirationTime > renderExpirationTime)
) {
  // ...
  return bailoutOnAlreadyFinishedWork(
    current,
    workInProgress,
    renderExpirationTime,
  );
}

如果能满足这个判断条件并且进入bailoutOnAlreadyFinishedWork,那么有极高的可能这个节点以及他的子树都不需要更新,React会直接跳过,我们使用新的context API的时候就是这种情况,但是使用老的context API是永远不可能跳过这个判断的

老的context API使用过程中,一旦有一个节点提供了context,那么他的所有子节点都会被视为有side effect的,因为React本身并不判断子节点是否有使用context,以及提供的context是否有变化,所以一旦检测到有节点提供了context,那么他的子节点在执行hasLegacyContextChanged的时候,永远都是true的,而没有进入bailoutOnAlreadyFinishedWork,就会变成重新reconcile子节点,虽然最终可能不需要更新DOM节点,但是重新计算生成Fiber对象的开销还是又得,一两个还好,数量多了时间也是会被拉长的。

以上就是使用老的context API比新的API要慢很多的原因,大家可以先不深究得理解一下,在我之后的源码分析环节会有更详细的讲解。

查看原文

赞 1 收藏 0 评论 1

Jokcy 发布了文章 · 2018-09-19

React性能分析利器来了,妈妈再也不用担心我的React应用慢了

Profiler

React16.5正式在devtool中加入了Profiler功能,用于收集每次变更导致的渲染时间,帮助开发者发现潜在的性能问题,有助于开发更高性能的React应用

官方博客

如何使用

在Chrome的开发工具插件react devtool中多了一个Profiler的tab,点击可以切换到Profiler界面

在你使用React16.5之后的版本中,开发时默认开启Profiler功能,要想在正是环境也使用这个功能,可以看这里

默认打开Profiler什么都没有,要点击录制按钮进行记录(跟chrome的performance挺像)。

在你开始录制之后你可以进行一些你想分析的操作,然后再点击stop来停止录制,就可以得到录制的内容

录制内容分析

首先要了解一点,React在16版本之后处理任务分为两个阶段:

  1. render阶段判断哪些变更需要被处理成DOM,也就是比较上一次渲染的结果和新的更新
  2. commit阶段React最终达成所有变更(也就是从js对象到DOM的更新),并且会调用componentDidMountcomponentDidUpdate这些生命周期方法

开发工具中通过commit阶段对性能数据进行编组,会显示在右侧工具栏上

看上去像一个柱状图,每一个柱子代表一次commit,他的颜色和高度对应执行时长,越高颜色越黄代表时间越长,反之越短。

删选commits

可以通过commits分组左边的设置图标点击出现的对话框设置删选选项,可以设置:

  1. 执行时间少于多少的不展示
  2. 是否显示原声DOM渲染的时间

火焰图

火焰图部分会以一个类似树形的结构显示一次commit过程中整个每个组件的渲染信息,跟commit分组信息类似,颜色和长短对应这个组件的渲染耗时,当然组件的渲染时间需要依赖他的子组件的渲染时间。

上图中可见Router组件渲染时间最长,也基本等于NavRoute的渲染时间之和,因为子组件的渲染最终肯定会被计算在父组件的渲染时间内。

你可以点击任何一个组件来查看他的详细信息

点击一个组件还可以查看他的propsstate

有些时候你选中一个组件,在commits分组中进行切换,在stateprops面板会有内容变化的提示

排名视图

选中火焰图边上的排名视图,会展示该次commit中组件渲染时间由高到低的排名,方便删选最长时间的渲染。

组件视图

如果你需要查看在你进行录制的过程中,某个组件被渲染了多少次,每次所用的时间,那么这个视图就是为你准备的。

一看像一个柱状图,每一条代表一次渲染,长度和颜色代表时间的长短。

你可以通过双击一个组件,或者选中一个组件,点击右上角的图标来打开该视图。

互动?(Interactions不知道怎么翻译好)

在之前React已经发布了一组实验API来追踪更新的原因,使用这个API的信息也会在devtool中展示

途中四个绿点就是调用API的节点对应的commit,同时你也可以在火焰图的commit信息中看到在这个commit存在的活动追踪

你可以在互动commits之间进行切换通过点击对应的信息

顺带提一下,这个API通过scheduler这个包使用

import { unstable_trace as trace } from "schedule/tracing"

class MyComponent extends Component {
  handleLoginButtonClick = event => {
    trace("Login button click", performance.now(), () => {
      this.setState({ isLoggingIn: true });
    });
  };

  // render ...
}

更详细的使用可以看这里

最后

这个功能非常棒,它能够让开发者非常直观的看到他的每次操作带来的组件渲染消耗,能够很方便帮助开发者发现一些不必要的渲染。

其实在React开发中一些小细节可能会给整个应用的性能开销带来很大的影响,只是现在的浏览器太强大以及大部分应用都处于性能过剩的情况下,所以性能问题不是特别明显。

原文的最后有一个视频展示了如何利用Profiler帮助发现性能问题并优化的案例,大家都可以看一下。

这个更新正好处于我正在阅读源码的时候,所以我会在自己的源码分析里面添加进去,源码分析的成果应该马上就好了,如果大家有兴趣可以关注我。

查看原文

赞 0 收藏 0 评论 0

Jokcy 发布了文章 · 2018-09-11

React源码阅读之:Controlled Input

不知道大家对于Controlled Input的概念好不好奇,我在最开始用React的时候就对其非常感兴趣,然而奈何那时候能力不够,也没那么多时间去看源码,所以一直处于猜测而没有去证实的阶段。在后来使用Vue进行开发的时候,我还自己实现过类似的组件,那时候是通过preventDefault keyDown事件来阻止自动更新,实现了大致的功能,所以当时觉得这可能也是React的做法吧。现在看了源码之后,发现自己还是too young,React根本就不是这么做的,我还花了很多时间去找他阻止默认行为的证据,结果才发现走了歪路。闲话到此为止,我们来看看React中如何实现Controlled Input

在看这个之前最好了解过React的事件系统,相关内容我已经准备好,但是还没整理成文章,大家可以先去自行了解下,我更新上来之后会在这里放链接,所以也可以关注我获取最新内容。

先来看一下batchedUpdates的代码:

function batchedUpdates(fn, bookkeeping) {
  if (isBatching) {
    return fn(bookkeeping);
  }
  isBatching = true;
  try {
    return _batchedUpdates(fn, bookkeeping);
  } finally {
    isBatching = false;
    var controlledComponentsHavePendingUpdates = needsStateRestore();
    if (controlledComponentsHavePendingUpdates) {
      _flushInteractiveUpdates();
      restoreStateIfNeeded();
    }
  }
}

finally里面执行了一个方法叫做restoreStateIfNeeded,如果你通过代码调试把debugger放之前,并且你写一个不更新state的demo,你会发现,在执行这个方法之前,input里面的内容其实是先变了的,然后执行完之后再变回来。哈哈,现在你应该能大致猜到了吧,是的,React的做法并不关心event,他等内容执行完了,然后看是否需要回滚再滚回来。

那么接下去我们就来看看细节

function restoreStateIfNeeded() {
  if (!restoreTarget) {
    return;
  }
  var target = restoreTarget;
  var queuedTargets = restoreQueue;
  restoreTarget = null;
  restoreQueue = null;

  restoreStateOfTarget(target);
  if (queuedTargets) {
    for (var i = 0; i < queuedTargets.length; i++) {
      restoreStateOfTarget(queuedTargets[i]);
    }
  }
}

这里涉及两个公共变量:restoreTargetrestoreQueue,那么这两个变量会在什么情况下变化呢?

在同一个文件ReactControlledComponent里面有这么一个方法

export function enqueueStateRestore(target) {
  if (restoreTarget) {
    if (restoreQueue) {
      restoreQueue.push(target);
    } else {
      restoreQueue = [target];
    }
  } else {
    restoreTarget = target;
  }
}

那么这个方法什么时候调用呢?答案是在ChangeEventPlugin里面

ChangeEventPlugin = {
  extractEvents() {
    // ...
    const event = createAndAccumulateChangeEvent(
      inst,
      nativeEvent,
      nativeEventTarget,
    );
  }
}

function createAndAccumulateChangeEvent(inst, nativeEvent, target) {
  const event = SyntheticEvent.getPooled(
    eventTypes.change,
    inst,
    nativeEvent,
    target,
  );
  event.type = 'change';
  // Flag this event loop as needing state restore.
  enqueueStateRestore(target);  // 这里
  accumulateTwoPhaseDispatches(event);
  return event;
}

我们先不关心这里具体是什么含义,我们只需要知道只有change event才需要回滚内容,所以在生成change事件的时候会把对应的节点。而生成事件的时间和batchUpdates是一起的,这就把整个内容串联起来了

inpute content -> trigger change event -> create event object -> enqueueStateRestore -> events trigger finsihed -> restoreStateIfNeeded

如何restore

function restoreStateOfTarget(target) {
  var internalInstance = getInstanceFromNode(target);
  if (!internalInstance) {
    // Unmounted
    return;
  }
  var props = getFiberCurrentPropsFromNode(internalInstance.stateNode);
  fiberHostComponent.restoreControlledState(internalInstance.stateNode, internalInstance.type, props);
}

在事件系统执行完之后,获取input节点对应的Fiber对象,然后读取新的props,这里的props是执行完绑定事件之后的最新的props,这是对应controlled component真正应该显示的props,然后再去对比props里面的内容和输入框内的实际内容,如果需要退回就执行。

function restoreControlledState(domElement, tag, props) {
  switch (tag) {
    case 'input':
      restoreControlledState(domElement, props);
      return;
    case 'textarea':
      restoreControlledState$3(domElement, props);
      return;
    case 'select':
      restoreControlledState$2(domElement, props);
      return;
  }
}

function restoreControlledState(element, props) {
  var node = element;
  updateWrapper(node, props);
  updateNamedCousins(node, props);
}

function updateWrapper(element, props) {
  var node = element;

  updateChecked(element, props);

  var value = getSafeValue(props.value);

  // 更新的重点在这里
  if (value != null) {
    if (props.type === 'number') {
      if (value === 0 && node.value === '' ||
      // eslint-disable-next-line
      node.value != value) {
        node.value = '' + value;
      }
    } else if (node.value !== '' + value) {
      node.value = '' + value;
    }
  }

  if (props.hasOwnProperty('value')) {
    setDefaultValue(node, props.type, value);
  } else if (props.hasOwnProperty('defaultValue')) {
    setDefaultValue(node, props.type, getSafeValue(props.defaultValue));
  }

  if (props.checked == null && props.defaultChecked != null) {
    node.defaultChecked = !!props.defaultChecked;
  }
}

后面具体更新的细节就不分析了,相信大家都看的懂(可以忽略一些不重要的函数)

查看原文

赞 2 收藏 2 评论 0

Jokcy 发布了文章 · 2018-09-05

React源码阅读之:复合类型方案设计

关于React中一些代码设计

最近在看React的源码,注意到了一些有意思的细节,比如经常会出现的一下比较和赋值代码

workInProgress.effectTag |= Ref
(workInProgress.effectTag & DidCapture) !== NoEffect

对于平时基本上没怎么用到过移位运算的我一开始表示这是啥?为啥要这么设计?

我们先来看一下,这个effectTag具体会有那些值

// Don't change these two values. They're used by React Dev Tools.
export const NoEffect = /*              */ 0b00000000000;
export const PerformedWork = /*         */ 0b00000000001;

// You can change the rest (and add more).
export const Placement = /*             */ 0b00000000010;
export const Update = /*                */ 0b00000000100;
export const PlacementAndUpdate = /*    */ 0b00000000110;
export const Deletion = /*              */ 0b00000001000;
export const ContentReset = /*          */ 0b00000010000;
export const Callback = /*              */ 0b00000100000;
export const DidCapture = /*            */ 0b00001000000;
export const Ref = /*                   */ 0b00010000000;
export const Snapshot = /*              */ 0b00100000000;

// Union of all host effects
export const HostEffectMask = /*        */ 0b00111111111;

export const Incomplete = /*            */ 0b01000000000;
export const ShouldCapture = /*         */ 0b10000000000;

这么一看貌似好像有点意思,可以看到大部分的值都只有一位是1,其他位都是00bxxx是原生二进制字面量的表示方法

那么回过头去我们再看上面两句表达式

workInProgress.effectTag |= Ref
// 也就是
workInProgress.effectTag = workInProgress.effectTag | RefRef

我们随便拿两个值来举例,比如PlacementUpdate,也就是0b00000000010 | 0b00000000100那么得到的结果是什么呢?0b00000000110,也就等于PlacementAndUpdate。所以这时候大家知道为什么大部分的值1所在的位置不一样了吧,因为其实每一位的1代表一种属性,把他们结合在一起就代表有多种属性,不会有重复。

同样的对于第二个表达式

(workInProgress.effectTag & DidCapture) !== NoEffect

我们拿UpdateDidCapture来进行&操作,那么得到的结果就很明显了,所有位都是0,所以后期的&操作是用来判断在某个变量中是否含有某个属性的。比如这里就是判断workInProgress.effectTag中是否含有DidCapture这个属性。

这种设计方式我觉得挺有参考意义的,可以用在类似权限系统上。大概现在很多权限系统已经这么做了吧,只是我不知道。。。

React源码正在阅读中,有望一两个月把所有成果放出来,有兴趣的可以关注我

查看原文

赞 9 收藏 5 评论 1

Jokcy 发布了文章 · 2018-04-17

require和import的区别是什么?看这个你就懂了

requireimport
动态评估静态评估
再运行时报错再解析时报错
不是关键词是关键词

语法

CommonJs

dep.js

module.exports = {
    foo: function () {},
    bar: 'a'
}

app.js

var dep = require('dep')
console.log(dep.bar)
dep.foo()

ESM

dep.js

export foo function(){}
export const bar = 'a'

app.js

import { foo, bar } from 'dep'
console.log(bar)
foo()

加载方式的不同

使用require的时候,其实会将module的代码进行包装,变成如下样子的代码:

function (exports, require, module, __filename, __dirname) {
  const m = 1;
  module.exports.m = m;
}

然后在执行这个方法的时候,我们可以传入:

const module = { exports: {} }
const require = function(){/* ...some module load code here */}
// __filename, __dirname是require的时候提供的路径分析出来的
fun(module.exports, require, module, __filename, __dirname)

执行完成之后,就能通过module拿到方法中向外抛出的变量了。

所以我们可以看到,module、require、exports都不是全局变量,而是专门为这个模块使用的局部变量。

require的时候真正做的事情如下:

  1. Resolution / 解析路径
  2. Loading / 加载代码
  3. Wrapping / 包装
  4. Evaluation / 评估执行
  5. Caching / 缓存

简单来说就是根据require调用时传入的路径,首先要拿到真正的绝对路径(是相对目录的,还是node_modules下面的等等),然后读入代码,包装成上面显示的样子,然后传给vm进行评估执行,得到结果,最后进行缓存。

所以,在模块代码执行完之前,node根本不知道这个模块到底export出了什么东西,这也是和ESM最大的区别,因为ESM是基于关键字的模块化,是可以在解析的过程中就知道导出了什么。

在解析ESM模块的时候,在把代码传给VM执行之前,就可以得到一个叫做Module Record的内部结构,他保存了模块倒出的内容的列表,在你import {f} from f的时候,他其实在你引用的地方和倒出的地方的f之间建立了连接,即它们是指向同一内存的,即便是原始数据类型,你修改模块中的指也会导致引用处的变化。啥意思呢?

// dep.js
export let a = 1
setTimeout(() => a += 1, 500)

// app.js
import { a } from 'dep'
setTimeout(function () {
  console.log(a)
}, 1000)

输出的会是2,但是你用require,CommonJs模块来做,就会是1,因为CommonJs是普通的值传递或者引用传递

这就是require和import最大的区别

查看原文

赞 8 收藏 9 评论 2

Jokcy 发布了文章 · 2018-02-27

webpack4升级指北

最近发现还是有听过人看这篇文章,所以再来更新一下。首先现在webpack4已经稳定,各个插件和loader基本上直接装都可以版本匹配,比如html-webpack-plugin,如果有不匹配的看peerDependencies提醒,会提示你需要安装新的版本。还有就是extract-text-webpack-plugin作者好像不想维护了,现在可以用mini-css-extract-plugin,webpack5据说会集成css打包,就不需要插件了

2018年2月25日,刚过完年webpack就给了一个加班红包。webpack4经过1个月的缓冲期,终于发布了正式版,那么抛给广大开发者的问题又来了,我是不是要升级了呢?本文就站在一个之前用webpack3开发项目,现在打算升级到4的角度上,来讲一讲需要升级的内容。

安装

首先你要重新安装以下的依赖包:

  1. webpack4
  2. webpack-cli(用来启动webpack)
  3. html-webpack-plugin还没有更新,会出现compilation.templatesPlugin is not a function的错误,目前需要npm i webpack-contrib/html-webpack-plugin -D安装,官方说法是因为作者这段时间没空更新,所以他们自己fork了一份改了,现在先用这种方式用着,等作者有空了再合并进去。
  4. 其他各种配件升级,dev-server升级到3,各种loader你们自己看着办,如果是新装的应该是已经支持了的,如果在编译过程中报can not find xxx的错误,说明xxx对应的loader可能需要升级了,因为webpack4去掉了this.options的支持
  5. 看一下有没有peerDependencies的提醒,有的话升级以下插件。目前来说我剩下一个ajv-keywords@3.1.0 requires a peer of ajv@^6.0.0 but none is installed. You must install peer dependencies yourself.的莫名其妙提醒,暂时没什么影响,也没查到解决方法,所以先放着,你们如果重新安装包可能就没这个错了。

以上这些就是安装包的工作,基本就差不多了。

配置

mode

webpack加了一个mode配置,只有两个值development | production,对不同的环境他会提供不同的一些默认配置,比如开发环境下面默认启用optimization.namedModules(原NamedModulesPlugin,现已弃用),而生产环境默认使用optimization.noEmitOnErrors(原NoEmitOnErrorsPlugin,现已弃用)。

不同模式下的默认配置如下:

  1. 生产环境默认开启了很多代码优化(minify,splite等)
  2. 开发时开启注视和验证,并且自动加上了eval devtool
  3. 生产环境不支持watching,开发环境优化了重新打包的速度
  4. 生产环境开启模块串联(原ModuleConcatenationPlugin),没用过不多说
  5. 自动设置process.env.NODE_ENV到不同环境,也就是不需要DefinePlugin来做这个了
  6. 如果你给mode设置为none,所有默认配置都去掉了

如果不加这个配置webpack会出现提醒,所以还是加上吧

CommonsChunkPlugin

相信大家如果听说过webpack4的更新,最大的感触应该就是去除了CommonsChunkPlugin,毕竟官方change log写的篇幅最多的就是这个

CommonsChunkPlugin删除之后,改成使用optimization.splitChunks进行模块划分,详细文档看这里

官方的说法是默认设置已经对大部分用户来说非常棒了,但是需要注意一个问题,默认配置只会对异步请求的模块进行提取拆分,如果要对entry进行拆分,需要设置optimization.splitChunks.chunks = 'all'。其他的内容大家就自己研究吧。

对应之前我们拆分runtime的情况,现在也有一个配置optimization.runtimeChunk,设置为true就会自动拆分runtime文件

UglifyJsPlugin

现在也不需要使用这个plugin了,只需要使用optimization.minimize为true就行,production mode下面自动为true

optimization.minimizer可以配置你自己的压缩程序

其他

还有很多是我们平时使用不太到的升级,更多的是一些性能上的优化和功能上的升级,附上官方change log,希望官方文档赶紧出来吧。

查看原文

赞 23 收藏 33 评论 5

Jokcy 提出了问题 · 2018-01-05

给网站上了certbot的https,却发现http可以正常访问,https却要翻墙访问

网站地址是jnode.jokcy.me,http正常访问,但是https需要翻墙才能访问。https是用certbot生成的,求大神指点

关注 1 回答 0

认证与成就

  • SegmentFault 讲师
  • 获得 76 次点赞
  • 获得 6 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 5 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

注册于 2015-03-31
个人主页被 1.3k 人浏览