HTTP/2轻松入门

作者:较瘦@毛豆前端

HTTP/2

1. 什么是HTTP/2

​ HTTP/2 是 HTTP 协议自 1999 年 HTTP 1.1 发布后的首个更新,主要基于 SPDY 协议。由互联网工程任务组(IETF)的 Hypertext Transfer Protocol Bis(httpbis)工作小组进行开发。该组织于2014年12月将HTTP/2标准提议递交至IESG进行讨论,于2015年2月17日被批准。HTTP/2标准于2015年5月以RFC 7540正式发表

2. HTTP/2 新特性

1) 二进制分帧

二进制分帧

​ HTTP/2 采用二进制格式传输数据,而非 HTTP/1.x 的文本格式,二进制协议解析起来更高效。 HTTP/1 的请求和响应报文,都是由起始行,首部和实体正文(可选)组成,各部分之间以文本换行符分隔。HTTP/2 将请求和响应数据分割为更小的帧,并且它们采用二进制编码。

​ HTTP/2 中,同域名下所有通信都在单个连接上完成,该连接可以承载任意数量的双向数据流。每个数据流都以消息的形式发送,而消息又由一个或多个帧组成。多个帧之间可以乱序发送,根据帧首部的流标识可以重新组装。

2)多路复用

​ 多路复用,代替原来的序列和阻塞机制。所有就是请求的都是通过一个 TCP连接并发完成。 HTTP 1.x 中,如果想并发多个请求,必须使用多个 TCP 链接,且浏览器为了控制资源,还会对单个域名有 6-8个的TCP链接请求限制

在 HTTP/2 中,有了二进制分帧之后,HTTP/2 不再依赖 TCP 链接去实现多流并行了,在 HTTP/2中:

1: 同域名下所有通信都在单个连接上完成。
2: 单个连接可以承载任意数量的双向数据流。
3: 数据流以消息的形式发送,而消息又由一个或多个帧组成,多个帧之间可以乱序发送,因为根据帧首部的流标识可以重新组装

这一特性,使性能有了极大提升:

  • 同个域名只需要占用一个 TCP 连接,消除了因多个 TCP 连接而带来的延时和内存消耗。

  • 单个连接上可以并行交错的请求和响应,之间互不干扰。

  • 在HTTP/2中,每个请求都可以带一个31bit的优先值,0表示最高优先级, 数值越大优先级越低。有了这个优先值,客户端和服务器就可以在处理不同的流时采取不同的策略,以最优的方式发送流、消息和帧。

3) 服务器推送

​ HTTP/2的另一个强大的新功能是服务器为单个客户端请求发送多个响应的能力。也就是说,除了对原始请求的响应之外,服务器还可以向客户端推送额外的资源,而不需要客户端明确请求每一个资源!例如服务端可以主动把JS和CSS文件推送给客户端,而不需要客户端解析HTML时再发送这些请求

4) 头部压缩

​ 每个HTTP传输都包含一组描述传输资源及其属性的标题。在HTTP/1.x中,此元数据始终以纯文本形式发送,并且每次传输的开销都会在任何位置增加500-800字节,如果使用HTTP Cookie,则会增加数千字节。为了减少这种开销并提高性能,HTTP/2使用两种简单但强大的技术使用HPACK压缩格式来压缩请求和响应头元数据

3. 结语

​ HTTP/2通过支持请求与响应的多路复用来减少延迟,通过压缩HTTP首部字段将协议开销降至最低,同时增加对请求优先级和服务器端推送的支持。

由一道'恶搞'排序引发的思考

作者:随便@毛豆前端

背景

​ 偶然在技术群里看到一个恶搞的排序。第一眼看着觉得这个排序算法是恶搞的,但是恶搞的排序算法却给出了正确的排序结果。不禁让人想去进一步了解它的运行原理。

Event Loop介绍

​ 我们知道JavaScript是单线程的,程序运行时,只有一个线程存在。单线程的JavaScript一段一段的执行,前面的执行完了,再执行后面的。但是如果遇到一个耗时很久的任务,比如接口请求、I/O操作,此时后面的任务如果一直等待,不仅浪费资源,并且页面会卡住,交互也很差。为了解决这个问题,JavaScript将任务分为同步和异步任务,来进行不同的处理。

​ JavaScript在执行时,会将同步任务在执行栈(execution context stack)中,按照顺序在主线程上执行,前面的任务执行完了,再执行后面的。遇到异步任务不会停下来等待,而是将其挂起,继续执行当前栈中的同步任务。当异步任务有返回结果时,将异步任务执行完成后的结果加入任务队列(task queue),通常任务队列中存放的都是异步完成后的回调函数。

​ 当执行栈中的任务完成后,空闲的主线程就会读取任务队列中是否有任务。如果有,主线程就会把最先进入任务队列的任务加入到执行栈中,执行栈中的任务执行完成后,主线程便又会去查询任务队列中的任务,并读取到执行栈中去执行。这个过程是循环往复的,这便是Event Loop,事件循环。

​ 网上有一张流传很广的图对这一过程进行了总结:

​ 由图可知,JavaScript在运行时产生堆和栈,ajax、setTimeout等异步任务被挂起,异步任务返回结果加入到任务队列,主线程会循环往复的读取任务队列中的任务,并加入执行栈中执行。

“排序”代码分析

​ OK,说了那么多,我们终于看到跟这个排序算法有关系的关键词了:setTimeout。作为一个异步任务,在循环数组的过程中,每次根据当前值,延时往任务队列添加回调函数,填充数据到新的数组中。由于值有大小的分别,小的值延时时间短,就会被先添加到任务队列中,最终利用Event Loop的时间差,达到了排序的目的。

​ 不考虑延时的问题,单纯从代码来看,完成整个排序只是用了一遍循环,这个排序代码的时间复杂度其实是O(n)的。并且从目前的例子来看:排序过程中,相同大小的数据在排序前后不会改变顺序,是稳定的排序算法。我们甚至可以把这段代码封装修改一下,通过一个回调来得到排序后的数组:

function eventLoopSort(list, callback) {
  var newList = [];
  var sum = 0;
  list.forEach(item => {
    setTimeout(() => {
      newList.push(item);
    }, item * 100)
    sum += item;
  });
  setTimeout(() => {
    callback && callback(newList);
  }, sum * 100);
}

var list = [1, 1, 4, 6, 2, 3, 9, 8, 7];
eventLoopSort(list, data => {
  console.log(data) // [1, 1, 2, 3, 4, 6, 7, 8, 9]
})

问题总结

​ 一本正经的讲了这么多,还是改变不了这个排序是恶搞,不可以应用到实际代码中的现实。因为在面对大量数据时,除了setTimeout延时较长之外,这个排序还是会出错的。考虑到代码本身执行也是需要耗时的,在面对大量数据时,前面数据执行时间较长,长到可以抵消延时的时间差的时候,排序就会完全出错了。

​ 最后,第一次写出这段”玩笑”代码的人,一定对浏览器的运行机制非常的了解。这也是我想通过这篇文章表达的意思,希望能通过这个恶搞,有趣的形式,对你的学习有所帮助。

JavaScript函数之参数解析

作者:一介书生@毛豆前端

在JavaScript世界中函数是一等公民,它不仅拥有一切传统函数的使用方式(声明和调用),而且可以做到像简单值一样赋值、传参、返回,这样的函数也称之为第一级函数(First-class Function)。不仅如此,JavaScript中的函数还充当了类的构造函数的作用,同时又是一个Function类的实例(instance)。这样的多重身份让JavaScript的函数变得非常重要。本次讲一下有关函数参数的细节知识。

函数形参的默认值

es5中模拟默认参数:

function makeRequest(url, time, callback) {
    time = (typeof time !== 'undefined') ? time : 2000;
    callback = (typeof callback !== "undefined") ? callback : function () {
        //
    }
}

在这个函数中timeout,callback为可选参数,如果不传系统会给他们赋予默认值。这种写法虽然很严谨,但是需要额外的代码。
es6中模拟默认参数:

function makeRequest(url, time=2000, callback=function () {}) {
  //
}

在es6的标准中,只有url是必传参数,其余参数都是可选参数。如下调用都可以生效。

makeRequest('/foo)
makeRequest('/foo, 500)
makeRequest('/foo, 500, function() {//...})

默认参数对arguments对象的影响

function mixArgs(first, second) {
    console.log(first === arguments[0])
    console.log(second === arguments[1])
    first = 'c'
    second = 'd'
    console.log(first === arguments[0])
    console.log(second === arguments[1])
}

结果:

true  
true  
true  
true

在非严格模式下,命名参数的变化会同步到arguments对象中,当first,second被赋予新值时arguments[0],arguments[1]也跟着更新了。但是在严格模式下,情况变得不一样。

function mixArgs(first, second) {
    'use strict'
    console.log(first === arguments[0])
    console.log(second === arguments[1])
    first = 'c'
    second = 'd'
    console.log(first === arguments[0])
    console.log(second === arguments[1])
}
mixArgs('a', 'b')

结果如下:

true  
true  
false  
false

严格模式下,无论参数如何变化,arguments对象是不变的。
在es6中,如果一个函数使用了默认参数值,则无论是否显式定义了严格模式,arguments对象的行为都将于es5的严格模式保持一致。

function mixArgs(first, second='b') {
    console.log(arguments.length)
    console.log(first === arguments[0])
    console.log(second === arguments[1])
    first = 'c'
    second = 'd'
    console.log(first === arguments[0])
    console.log(second === arguments[1])
}
mixArgs('a')

结果如下:

1  
true  
false
false
false

默认参数的临时死区

es6的let和const是存在临时死区TDZ,默认参数也存在同样的临时死区,在这里参数不可访问。与let声明类似,定义参数时会为每个参数创建一个新的标识符绑定,更改绑定在初始化时不可被访问。

function add(first = second, second) {
	return first + second
}
add(1, 1)
add(undefined, 1)

add(1, 1)相当于执行以下代码:

let first = 1
let second = 1

add(undefined, 1)相当于执行以下代码:

let first = second
let second = 1

在初始化first时,second尚未初始化,所以导致程序报错。

不定参数

在函数的命名参数前加“…”就表明这是个不定参数,该参数作为一个数组,包含字它之后传入的所有参数。

function pick(obj, ...keys) {
	let result = {}
	for (let i = 0; i < keys.length; i++) {
		result[keys[i]] = obj[keys[i]]
	}
	return result
}

这个函数模仿了Undescore.js的pick()方法,返回一个给定对象的副本。示例中只定义了一个参数是被复制的原始对象,其他参数为被复制属性的名称。

var article = {
	title: 'js-函数',
	author: 'zh',
	age: '20',
}
console.log(pick(article, 'title', 'age'))

结果:

{title: "js-函数", age: "20"}

此时函数的length属性统计的是函数的命名参数的数量,不定参数加入不会影响length属性的值。示例中pick的length为1,因为它值计算obj。

不定参数的使用有两条限制:

    1. 首先每个函数最多只能声明一个不定参数,不定参数的位置一定要放在所有参数的末尾。
    1. 不定参数不能用于对象字面量setter之中。如下写法是报错的。
let object = {
	set a(...val) {
		//
    }
}

下节我们会讨论函数的箭头函数,下次再见。

React Hooks早知道

作者:docoder@毛豆前端

React Hooks

Notes

  • Example

    function Example() {
      const [count, setCount] = useState(0);
      useEffect(() => {
        document.title = `点击了 ${count} 次`;
      });
      return (
        <div>
          <p>点击了 {count}</p>
          <button onClick={() => setCount(count + 1)}>
            点击
          </button>
        </div>
      );
    }
    
  • React 16.8

  • No Breaking Changes
    • 可选的
    • 100% 向后兼容
    • 立即可用
  • Class 不会被移除

  • Hook 被 React 团队 期望成为未来写 React 的主要方式

  • 除了不常用的 getDerivedStateFromError 和 componentDidCatch 在 hook 中还没有被等价实现(很快会添加),其他几乎可以覆盖所有使用 Class 的情况

  • 不影响对React的理解

  • 未来 Redux connect() 和 React Router 也会使用类似 useRedux() 或 useRouter() 的自定义 Hooks,当然现在的 API 用法也是兼容的

  • 对静态类型支持更好,如 TypeScript

  • 性能更好

    • 避免了 Class 的开销
    • 重用逻辑无需高阶组件,减少层级
  • 逻辑重用

    • render props
    • 高阶组件
    • 重构,抽取通用逻辑,不用重写层级结构
  • 拥抱 function, 无 Class

  • Thinking in Hooks

  • 不能在 Class 中使用

  • 创建自己的 Hooks

    • 每一次调用都会得到一个完全孤立的状态
    • 可以在同一个组件中使用两次相同的自定义 Hook
    • 约定大于特性
      • 以 “use” 开头
        • 只用于 linter plugin
        • react 并没有依赖此来搜索 hook 或完成其他功能特性
  • Hook 让我们根据代码的作用进行拆分

    • 多次使用,粒度更小
  • Hooks 只能在顶层调用

    • 不能在循环,条件语句,嵌套函数中调用

    • 保证其顺序性和正确性

      • 保证每次 render 都按同样的顺序执行

      • 在多个 useState 和 useEffect 调用过程中保证 state 的正确性

      • React 依赖 Hooks 的调用顺序来确保 state 和 useState 的对应

        • 通过类似数组的方式实现,每次 render 是按数组 index 进行对应
        // ------------
        // First render
        // ------------
        useState('Mary')           // 1. Initialize the name state variable with 'Mary'
        useEffect(persistForm)     // 2. Add an effect for persisting the form
        useState('Poppins')        // 3. Initialize the surname state variable with 'Poppins'
        useEffect(updateTitle)     // 4. Add an effect for updating the title
              
        // -------------
        // Second render
        // -------------
        useState('Mary')           // 1. Read the name state variable (argument is ignored)
        useEffect(persistForm)     // 2. Replace the effect for persisting the form
        useState('Poppins')        // 3. Read the surname state variable (argument is ignored)
        useEffect(updateTitle)     // 4. Replace the effect for updating the title
              
        // ...
        
    • linter plugin 会进行验证

  • 只能在 React functions 中调用, 不能在普通的 JavaScript 函数中调用

    • React 的函数式组件
    • 自定义的 Hook
  • linter plugin

  • shouldComponentUpdate

    • React .memo wrap 一个 function component 会浅比较 props

      • 仅比较属性,因为不存在 single state object to compare
      • 第二个参数接收一个自定义的 comparison function
      • 返回 true,update 将被跳过
      const Button = React.memo((props) => {
        // your component
      });
      
    • useMemo

  • getDerivedStateFromProps

    function ScrollView({row}) {
      let [isScrollingDown, setIsScrollingDown] = useState(false);
      let [prevRow, setPrevRow] = useState(null);
      
      if (row !== prevRow) {
        // Row changed since last render. Update isScrollingDown.
        setIsScrollingDown(prevRow !== null && row > prevRow);
        setPrevRow(row);
      }
      
      return `Scrolling down: ${isScrollingDown}`;
    }
    

Hooks

  • state hook

    • useState

      const [state, setState] = useState(initialState);
      
    • 多个State,多次使用useState

      • 数组解构,赋予状态变量不同的名字

      • 在每一次渲染中以相同的顺序被调用

      • initialState 不必须是对象,实际上不鼓励是对象,根据实际数据相关性,进行分组和分离。这样也更利于之后代码重构,抽取相关逻辑成一个自定义 Hook

        //👎
        const [state, setState] = useState({ left: 0, top: 0, width: 100, height: 100 });
              
        //👍改为如下:
        const [position, setPosition] = useState({ left: 0, top: 0 });
        const [size, setSize] = useState({ width: 100, height: 100 });
              
        //重构,自定义 Hook:
        function Box() {
          const position = useWindowPosition();
          const [size, setSize] = useState({ width: 100, height: 100 });
          // ...
        }
              
        function useWindowPosition() {
          const [position, setPosition] = useState({ left: 0, top: 0 });
          useEffect(() => {
            // ...
          }, []);
          return position;
        }
        
    • state 仅在第一次 render 时被创建,之后只是修改使我们得到最新的 state

    • 无需像 useEffect 或 useCallback 那样指定 依赖列表,由 React 来保证

      • 未来,React也会移除 useEffect 和 useCallback 的依赖列表,因为这完全可以通过算法自动解决
    • 函数式更新 (Functional updates)

      • 新值是通过之前的值计算而来

      • setCount 可以接受一个函数,接受之前的 state, 返回新的 state

        function Counter({initialCount}) {
          const [count, setCount] = useState(initialCount);
          return (
            <>
              Count: {count}
              <button onClick={() => setCount(initialCount)}>Reset</button>
              <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
              <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
            </>
          );
        }
        
    • state 更新了相同的值,不会进行 render 或 触发 effect

      • Object.is() 算法,进行比较

      • useMemo

      • forceUpdate ?

        • 避免使用

        • hack

          const [ignored, forceUpdate] = useReducer(x => x + 1, 0);
                  
          function handleClick() {
            forceUpdate();
          }
          
    • 懒初始化

      • useState 接受一个函数,返回 initialState,仅当初始 render 时被调用进行初始化,只调用一次

        const [state, setState] = useState(() => {
          const initialState = someExpensiveComputation(props);
          return initialState;
        });
        
  • effect hook

    • useEffect

      useEffect(
        () => {
          // side effects (获取数据、设置订阅和手动更改 React 组件中的 DOM 等)
          return () => { // 可选
            // clean up
          }
        }
       	,
        [state, ...] // 可选,仅当 state (或 ...) 改变时,effect 才重新运行
      );
      
    • 将类组件中的 componentDidMountcomponentDidUpdatecomponentWillUnmount 统一为一个 API

      • 避免逻辑分散和重复书写
        • 生命周期函数常常包含不相关的逻辑,同时相关的逻辑被拆分进不同的方法
    • 在每一次 render 后运行 effects ( 包括第一次 render )

      • React 保证每次运行 effects 之前 DOM 已经更新了
      • 在每次 render 的时候,都是新创建了一个函数传递给了 useEffect
        • 每次都是创建了新的 effect 替换之前的。
        • 每个 effect 属于一个特定的 render
        • 不用担心 effect 里 state 过期的问题
    • 拥抱闭包,在函数作用域中,可方便访问 state

    • 通过返回一个函数来 clean up

      • 添加和清理的逻辑可以彼此靠近

      • 在下次运行 effect 之前清理上一次 render 中的 effect

        • 清理不在 unmount 调用一次,而是在每次 re-render 后调用

          • 避免在缺失 componentDidUpdate 时会产生的 bugs

            //当 friend 的属性改变时,会产生依旧显示之前 friend 的在线状态的bug,尤其当 unmounting 的时候,由于 unsubscribe 一个错误的 friend id 会产生内存泄露甚至 crash
            	componentDidMount() {
                ChatAPI.subscribeToFriendStatus(
                  this.props.friend.id,
                  this.handleStatusChange
                );
              }
                      
            	componentWillUnmount() {
                ChatAPI.unsubscribeFromFriendStatus(
                  this.props.friend.id,
                  this.handleStatusChange
                );
              }
            
            //需要使用 componentDidUpdate
            	componentDidMount() {
                ChatAPI.subscribeToFriendStatus(
                  this.props.friend.id,
                  this.handleStatusChange
                );
              }
                      
              componentDidUpdate(prevProps) {
                // Unsubscribe 之前的 friend.id
                ChatAPI.unsubscribeFromFriendStatus(
                  prevProps.friend.id,
                  this.handleStatusChange
                );
                // Subscribe 新的 friend.id
                ChatAPI.subscribeToFriendStatus(
                  this.props.friend.id,
                  this.handleStatusChange
                );
              }
                      
              componentWillUnmount() {
                ChatAPI.unsubscribeFromFriendStatus(
                  this.props.friend.id,
                  this.handleStatusChange
                );
              }
                      
            
        • 可以有选择的运行 effect,从而避免性能问题

          • useEffect 的第二个参数

            useEffect(() => {
              function handleStatusChange(status) {
                setIsOnline(status.isOnline);
              }
                      
              ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
              return () => {
                ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
              };
            }, [props.friend.id]); // 仅当 props.friend.id 改变时 effect 才运行
            
          • 仅在 mount 和 unmount 才运行 effect

            • 第二个参数传空数组 []
          • 错误的指定第二个参数(经常是指过少配置), 会造成得不到最新的 props 或 state 的 bug

        • 把函数放到 useEffect 里, 这样更安全

          function Example({ someProp }) {
            function doSomething() {
              console.log(someProp);
            }
                  
            useEffect(() => {
              doSomething();
            }, []); // 🔴 This is not safe (it calls `doSomething` which uses `someProp`)
          }
          
          function Example({ someProp }) {
            useEffect(() => {
              function doSomething() {
                console.log(someProp);
              }
                  
              doSomething();
            }, [someProp]); // ✅ OK (our effect only uses `someProp`)
          }
                  
          function Example({ someProp }) {
            useEffect(() => {
              function doSomething() {
                console.log('hello');
              }
                  
              doSomething();
            }, []); // ✅ OK in this example because we don't use *any* values from component scope
          }
          
        • 不能将函数放到 effect 里

          • 确保函数中没有引用 props 或 state

          • 纯计算函数,将该函数返回结果作为 effect 的依赖

          • 用 useCallback wrap 函数 (确保函数在依赖不变的情况下,本身不变),再作为 effect 依赖

            function ProductPage({ productId }) {
              // ✅ Wrap with useCallback to avoid change on every render
              const fetchProduct = useCallback(() => {
                // ... Does something with productId ...
              }, [productId]); // ✅ All useCallback dependencies are specified
                      
              return <ProductDetails fetchProduct={fetchProduct} />;
            }
                      
            function ProductDetails({ fetchProduct })
              useEffect(() => {
                fetchProduct();
              }, [fetchProduct]); // ✅ All useEffect dependencies are specified
              // ...
            }
            
        • effect 依赖变动太频繁

          • 函数式更新 (Functional updates)
          function Counter() {
            const [count, setCount] = useState(0);
                  
            useEffect(() => {
              const id = setInterval(() => {
                setCount(count + 1); // This effect depends on the `count` state
              }, 1000);
              return () => clearInterval(id);
            }, []); // 🔴 Bug: `count` is not specified as a dependency
                  
            return <h1>{count}</h1>;
          }
          
          function Counter() {
            const [count, setCount] = useState(0);
                  
            useEffect(() => {
              const id = setInterval(() => {
                setCount(c => c + 1); // ✅ This doesn't depend on `count` variable outside. The identity of the setCount function is guaranteed to be stable so it’s safe to omit.
              }, 1000);
              return () => clearInterval(id);
            }, []); // ✅ Our effect doesn't use any variables in the component scope
                  
            return <h1>{count}</h1>;
          }
          
          const BookEntryList = props => {
            const [pending, setPending] = useState(0);
            const [booksJustSaved, setBooksJustSaved] = useState([]);
                  
            useEffect(() => {
              const ws = new WebSocket(webSocketAddress("/bookEntryWS"));
                  
              ws.onmessage = ({ data }) => {
                let packet = JSON.parse(data);
                if (packet._messageType == "initial") {
                  setPending(packet.pending);
                } else if (packet._messageType == "bookAdded") {
                  setPending(pending - 1 || 0);
                  setBooksJustSaved([packet, ...booksJustSaved]);
                } else if (packet._messageType == "pendingBookAdded") {
                  setPending(+pending + 1 || 0);
                } else if (packet._messageType == "bookLookupFailed") {
                  setPending(pending - 1 || 0);
                  setBooksJustSaved([
                    {
                      _id: "" + new Date(),
                      title: `Failed lookup for ${packet.isbn}`,
                      success: false
                    },
                    ...booksJustSaved
                  ]);
                }
              };
              return () => {
                try {
                  ws.close();
                } catch (e) {}
              };
            }, []);
                  
            //...
          };
          
          function scanReducer(state, [type, payload]) {
            switch (type) {
              case "initial":
                return { ...state, pending: payload.pending };
              case "pendingBookAdded":
                return { ...state, pending: state.pending + 1 };
              case "bookAdded":
                return {
                  ...state,
                  pending: state.pending - 1,
                  booksSaved: [payload, ...state.booksSaved]
                };
              case "bookLookupFailed":
                return {
                  ...state,
                  pending: state.pending - 1,
                  booksSaved: [
                    {
                      _id: "" + new Date(),
                      title: `Failed lookup for ${payload.isbn}`,
                      success: false
                    },
                    ...state.booksSaved
                  ]
                };
            }
            return state;
          }
          const initialState = { pending: 0, booksSaved: [] };
                  
          const BookEntryList = props => {
            const [state, dispatch] = useReducer(scanReducer, initialState);
                  
            useEffect(() => {
              const ws = new WebSocket(webSocketAddress("/bookEntryWS"));
                  
              ws.onmessage = ({ data }) => {
                let packet = JSON.parse(data);
                dispatch([packet._messageType, packet]); // The identity of the dispatch function from useReducer is always stable
              };
              return () => {
                try {
                  ws.close();
                } catch (e) {}
              };
            }, []);
                  
            //...
          };
          
        • exhaustive-deps ESLint

    • 异步请求数据

      function SearchResults() {
        const [data, setData] = useState({ hits: [] });
        const [query, setQuery] = useState('react');
          
        useEffect(() => { // 在 useEffect 里直接使用 async function 是不被允许的,因为 useEffect function 必须要返回一个清理 function 或 nothing。
          let ignore = false;
          
          async function fetchData() { //需要在 useEffect function 里使用 async function
            const result = await axios('https://hn.algolia.com/api/v1/search?query=' + query);
            if (!ignore) setData(result.data); //当组件 unmount 时,阻止其设置 state
          }
          
          fetchData();
          return () => { ignore = true; }
        }, [query]); // [query] 阻止造成循环,仅当 query 改变时,effect 才执行
          
        return (
          <>
            <input value={query} onChange={e => setQuery(e.target.value)} />
            <ul>
              {data.hits.map(item => (
                <li key={item.objectID}>
                  <a href={item.url}>{item.title}</a>
                </li>
              ))}
            </ul>
          </>
        );
      }
      
    • 关注点分离,多次使用 useEffect

      • 根据代码的作用拆分成多个 effect
    • useEffect 不会阻塞浏览器渲染

      • componentDidMountcomponentDidUpdate 会阻塞
      • 提供阻塞版本 useLayoutEffect ,来满足同步调用计算元素尺寸等问题
  • other hooks

    • useContext

    • useReducer

      • 只是对 local state 进行 redux 化,没有形成 store 和 公用的 state 树
      const [state, dispatch] = useReducer(reducer, initialArg, init);
      
      const initialState = {count: 0};
          
      function reducer(state, action) {
        switch (action.type) {
          case 'increment':
            return {count: state.count + 1};
          case 'decrement':
            return {count: state.count - 1};
          default:
            throw new Error();
        }
      }
          
      function Counter({initialState}) {
        const [state, dispatch] = useReducer(reducer, initialState);
        return (
          <>
            Count: {state.count}
            <button onClick={() => dispatch({type: 'increment'})}>+</button>
            <button onClick={() => dispatch({type: 'decrement'})}>-</button>
          </>
        );
      }
      
      • Pass down a dispatch function ```jsx const TodosDispatch = React.createContext(null);

      function TodosApp() { // Note: dispatch won’t change between re-renders const [todos, dispatch] = useReducer(todosReducer);

      return (
          <TodosDispatch.Provider value={dispatch}>
              <DeepTree todos={todos} />
          </TodosDispatch.Provider>
      ); } ``` ```jsx
      

      function DeepChild(props) { // If we want to perform an action, we can get dispatch from context. const dispatch = useContext(TodosDispatch);

      function handleClick() {
          dispatch({ type: 'add', text: 'hello' });
      }
      
      return (
          <button onClick={handleClick}>Add todo</button>
      ); } ```
      
    • useRef

      • 类似实例变量

        function Timer() {
          const intervalRef = useRef();
              
          useEffect(() => {
            const id = setInterval(() => {
              // ...
            });
            intervalRef.current = id; // current 可以被赋任何值,类似类中的实例变量
            return () => {
              clearInterval(intervalRef.current);
            };
          });
              
          // ...
          function handleCancelClick() {
            clearInterval(intervalRef.current);
          }
          // ...
        }
        
      • 获取 previous state

        function Counter() {
          const [count, setCount] = useState(0);
          const prevCountRef = useRef();
          useEffect(() => {
            prevCountRef.current = count;
          });
          const prevCount = prevCountRef.current;
              
          return <h1>Now: {count}, before: {prevCount}</h1>;
        }
        
      • 自定义 Hook,usePrevious

        • 可能之后会提供开箱即用的官方实现
        function Counter() {
          const [count, setCount] = useState(0);
          const prevCount = usePrevious(count); // 使用自定义的 Hook
          return <h1>Now: {count}, before: {prevCount}</h1>;
        }
        // 自定义 Hook
        function usePrevious(value) {
          const ref = useRef();
          useEffect(() => {
            ref.current = value;
          });
          return ref.current;
        }
        
      • 在一些异步回掉中,读取最新 state

        // 首先点击 Show alert, 然后点击 Click me
        function Example() {
          const [count, setCount] = useState(0);
              
          function handleAlertClick() {
            setTimeout(() => {
              alert('You clicked on: ' + count); //读取的是点击 Show alert 时的 count,不是最新值
            }, 3000);
          }
              
          return (
            <div>
              <p>You clicked {count} times</p>
              <button onClick={() => setCount(count + 1)}>
                Click me
              </button>
              <button onClick={handleAlertClick}>
                Show alert
              </button>
            </div>
          );
        }
        
        // 用 useRef 修改:
        function Example() {
          const [count, setCount] = useState(0);
                
          //使用 useRef
          const countRef = useRef();
          countRef.current = count;
                
          function handleAlertClick() {
            setTimeout(() => {
              alert('You clicked on: ' + countRef.current); //通过 ref 访问 count,读取的是最新值
            }, 3000);
          }
              
          return (
            <div>
              <p>You clicked {count} times</p>
              <button onClick={() => setCount(count + 1)}>
                Click me
              </button>
              <button onClick={handleAlertClick}>
                Show alert
              </button>
            </div>
          );
        }
        
      • 懒加载

        • useRef 不像 useState 那样接收函数

          function Image(props) {
            // ⚠️ IntersectionObserver is created on every render
            const ref = useRef(new IntersectionObserver(onIntersect));
            // ...
          }
          
          function Image(props) {
            const ref = useRef(null);
                  
            // ✅ IntersectionObserver is created lazily once
            function getObserver() {
              let observer = ref.current;
              if (observer !== null) {
                return observer;
              }
              let newObserver = new IntersectionObserver(onIntersect);
              ref.current = newObserver;
              return newObserver;
            }
                  
            // When you need it, call getObserver()
            // ...
          }
          
    • useCallback(fn, deps)

      • 返回一个带缓存的 callback

      • 仅当所需依赖 deps (数组) 中元素改变时,才执行,否则返回缓存的值

      • 等价于 useMemo(() => fn, deps)

      • exhaustive-deps ESLint

        // 使用 Callback Refs, 而不用 useRef,这样当 ref 改变时,可以获知
        function MeasureExample() {
          const [height, setHeight] = useState(0);
              
          const measuredRef = useCallback(node => {
            if (node !== null) {
              setHeight(node.getBoundingClientRect().height);
            }
          }, []); // [] 确保 ref callback 不会改变,re-renders 时,不会被重复调用执行
              
          return (
            <>
              <h1 ref={measuredRef}>Hello, world</h1>
              <h2>The above header is {Math.round(height)}px tall</h2>
            </>
          );
        }
        
        // 抽取自定义 Hook
        function MeasureExample() {
          const [rect, ref] = useClientRect();
          return (
            <>
              <h1 ref={ref}>Hello, world</h1>
              {rect !== null &&
                <h2>The above header is {Math.round(rect.height)}px tall</h2>
              }
            </>
          );
        }
              
        function useClientRect() {
          const [rect, setRect] = useState(null);
          const ref = useCallback(node => {
            if (node !== null) {
              setRect(node.getBoundingClientRect());
            }
          }, []);
          return [rect, ref];
        }
        
    • useMemo(() => fn, deps)

      • 返回一个带缓存的值,避免消耗性能的计算重复执行

        const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
        
        • 仅当 a,b 改变时,才会重新调用 computeExpensiveValue
        • 每次 render 时,传给 useMemo 的函数会执行,不要把一些副作用放到里面
      • 性能优化

        function Parent({ a, b }) {
          // Only re-rendered if `a` changes:
          const child1 = useMemo(() => <Child1 a={a} />, [a]);
          // Only re-rendered if `b` changes:
          const child2 = useMemo(() => <Child2 b={b} />, [b]);
          return (
            <>
              {child1}
              {child2}
            </>
          )
        }
        
    • useImperativeHandle(ref, createHandle, [deps])

      • 当使用 ref 来给父组件提供实例时,用来提供自定义的方法属性

      • 尽量避免使用

      • 应该与 forwardRef 一起使用

        function FancyInput(props, ref) {
          const inputRef = useRef();
          useImperativeHandle(ref, () => ({
            focus: () => {
              inputRef.current.focus();
            }
          }));
          return <input ref={inputRef} />;
        }
        FancyInput = forwardRef(FancyInput);
        
        function Parent() {
          const fancyInputRef = useRef();
          return (
            <>
              <FancyInput ref={fancyInputRef} />
              <button onClick={()=>{
                  fancyInputRef.current.focus()
              }}>Focus</button>
            </>
          )
        }
        
        

设计模式之组合模式

作者:葉@毛豆前端

组合模式

一、定义

组合模式,将对象组合成树形结构以表示“部分-整体”的层次结构,组合模式使得用户对单个对象和组合对象的使用具有一致性

在定义中提到了“部分-整体”、“单个对象”、“组合对象”这几个关键词,因此掌握组合模式的重点是要理解清楚 “部分/整体” 还有 ”单个对象“ 与 “组合对象” 的含义

二、作用

组合模式的作用即定义描述的那样,有两个作用:

  1. 将对象组合成树形结构,以表示“部分-整体”的层次结构
  2. 通过对象的多态性表现,使得用户对单个对象和组合对象的使用具有一致性

针对以上的两点,下面做一个详细说明,首先看一段代码:这是一个模拟电脑开机,运行应用程序的简单宏命令例子

let runWeChat = {
    run : function() {
        console.log("wechat已经启动")
    }
}

let runQQ = {
    run : function() {
        console.log("QQ已经启动")
    }
}

let runChrome = {
    run : function() {
        console.log("Google Chrome已经启动")
    }
}

let applicationCommand = function () {
    return {
        commandLists : [],
        addCommand: function (command) {
            this.commandLists.push(command)
        },
        run: function () {
            this.commandLists.map((item, index) => {
                item.run()
            })
        }
    }
}

let ac = applicationCommand ()

ac.addCommand(runWeChat)

ac.addCommand(runQQ)

ac.addCommand(runChrome)

ac.run()

  • 这样树形遍历的结构,通过调用组合对象的run方法,程序便会递归调用组合对象下面的子对象的run方法,因此我们只要按下电脑的开机键,微信,qq,谷歌浏览器这些程序就会被顺序运行。这样的方式很简洁的描述出了部分-整体的结构
  • 在这个组合模式中,利用对象的多态性表现,我们可以不关心组合对象和单个对象的区别,使用时统一使用组合结构中的所有对象,而不需要关心它是组合对象还是单个对象

这样的方式在实际的开发中可以带来很大的便利性。当我们在开机程序中添加命令时,无需关心这个是宏命令还是子命令,只关心这个命令是有可以执行的run方法,那么这个命令就可以被添加。执行的差异是在代码里体现的,对客户无感。

仍以上面的宏命令为例,请求在树最顶端的对象往下传递,如果此时处理请求的是普通子命令(叶子对象),这个时候叶子对象自己就会对请求做出处理;若此时处理请求的宏命令(组合对象),组合对象则遍历它的子对象,将请求传递下去。上面的例子只是简单的组合对象下只有叶子对象,叶子对象是树的最小单位,不会再有其他子节点,组合对象下可能还有子节点,如下图:

请求是由上至下沿着树进行传递,直到叶子对象,作为使用者,只需要关心树最顶层的组合对象,只要请求这个组合对象,请求便会沿着树往下传递,顺次到达所有的叶子对象

三、组合模式例子-员工部门关系

在公司里,必然存在员工的部门以及从属关系(这里只考虑每个人只属于一个部门),公司分为众多的部门,每个部门会有一个主管,带领下面的员工,这个员工可能只是最末端的一个职位,也有可能在他手下还有一波兄弟,每个部门是一个组合对象,部门下面的小组还是一个组合对象,这样就给构成了组合的模式,接下来我们来看下例子:

let leaderStaff = function (name, sex, apartment) {
    this.name = name;
    this.sex = sex;
    this.apartment = apartment;
    this.children = [];
}
leaderStaff.prototype.add = function (child) {
    this.children.push(child)
}
leaderStaff.prototype.doCount = function () {
    console.log("leader", this.name +"--"+ this.sex +"--"+ this.apartment)
    this.children.map((child, index) => {
        child.doCount()
    })
}   
let staff = function(name, sex, apartment) {
    this.name = name;
    this.sex = sex;
    this.apartment = apartment;
}
staff.prototype.add = function() {
    throw new Error("我还只是个孩子!!->_->")
}
staff.prototype.doCount = function () {
    console.log("普通员工",this.name +"--"+ this.sex +"--"+ this.apartment)
}
let leaderStaff1 = new leaderStaff('大旺', '男', 'A')
let leaderStaff2 = new leaderStaff('大张', '男', 'B')
let leaderStaff3 = new leaderStaff('大李', '男', 'C')
let leaderStaff4 = new leaderStaff('大无', '女', 'D')
let staff5 = new staff('小马', '', '0') 
let staff1 = new staff('小黑', '男', 'd')
let staff2 = new staff('小撒', '女', 'e')
let staff3 = new staff('小周', '女', 'f')
let staff4 = new staff('小郑', '男', 'g')

leaderStaff1.add(leaderStaff3);
leaderStaff1.add(leaderStaff2);
leaderStaff1.add(leaderStaff4)
leaderStaff1.add(staff1)

leaderStaff3.add(staff4)
leaderStaff3.add(staff3)
leaderStaff4.add(staff2)
// staff3.add(staff5)

leaderStaff1.doCount()

执行结果如图:

这样看起来似乎树形结构不是很明显,所以我们再看下中间执行的数据:

这里我们把拥有下级(子元素)的都统称为老板(leaderStaff),没有下级(子元素)的都叫做员工(staff),员工下面在没有子元素,否则抛出错误。他们都有doCount这个方法。很明显leaderStaff1(大旺)有四个子元素,其中大李又有两个子元素,以及大无有一个子元素,这些都是被称作组合对象,使用的人也不需要分辨他是孩子对象还是祖先元素。

这里我们改变树的结构,增加新的数据,却不用修改原来的代码,这是可取的,符合开放-封闭原则

四、注意事项

我们现在了解了组合模式,还要说下在使用组合模式的实收,有一些值得我们注意的地方:

  1. 组合模式不是父子关系 组合模式的树形结构容易让人误以为组合对象和叶子对象之间是父子关系,但并不是这样的。组合模式是一种HAS-A(聚合)的关系,而不是IS-A,组合对象把请求委托给它所包的所有叶对象,它们能够合作的关键是拥有相同是的接口

  2. 对叶对象操作的一致性 组合模式除了要求组合对象和也对象拥有相同的接口之外,还有一个必要条件,就是对一组也对象的操作必须具有一致性

  3. 双向映射关系 上面的例子中,公司给员工发送通知的步骤是从公司到各个部分,再到小组,最后才到每个员工的邮箱里,这便是一个组合模式的例子,但是存在特殊情况,一个员工同属于多个部门的时候就没有了严格意义上的层次结构,在这种情况下便不再适用组合模式,否则该员工很可能收到两份邮件

  4. 用职责链模式提高组合模式性能 一些情况下,生成树的结构十分复杂,节点数量众多,在遍历的过程中,消耗性能。职责链模式就是在遍历的过程中,只让请求顺着联调从父元素往子元素上传递,或子元素往父元素身上船体,知道遇到能处理请求的对象未知,避免浪费

五、总结

虽然多数时候组,合模式带给我们便利,让我们可以使用树形的方式去创建对象结构,还可以忽略组合对象和单个对象之间的差别,使用一致的方式处理。但是,它的缺点也需要注意:他们的区别只有在运行的时候才会体现出来。

AST原理分析

作者:东北烤冷面@毛豆前端

一、什么是AST

抽象语法树Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。

二、AST有什么作用

抽象语法树在很多领域有广泛的应用,比如浏览器,智能编辑器,编译器等。在JavaScript中,虽然我们并不会常常与AST直接打交道,但却也会经常的涉及到它。例如使用UglifyJS来压缩代码,bable对代码进行转换,ts类型检查,语法高亮等,实际这背后就是在对JavaScript的抽象语法树进行操作。

三、AST生成过程

javascript的抽象语法树的生成主要依靠的是Javascript Parser(js解析器),整个解析过程分为两个阶段:

1.词法分析(Lexical Analysis)

词法分析是计算机科学中将字符序列转换为单词(Token)序列的过程,进行词法分析的程序叫做词法分析器,也叫扫描器(Scanner)。

//code
let age='18'

//tokens
[
  {
    value: 'let',
    type:'identifier'
  },
  {
    type:'whitespace',
    value:' '
  },
  {
    value: 'age',
    type:'identifier'
  },
  {
    value: '=',
    type:'operator'
  },
  {
    value: '=',
    type:'operator'
  },
  {
    value: '18',
    type:'num'
  },
]

2.语法分析(Parse Analysis)

语法分析是编译过程的一个逻辑阶段。语法分析的任务是在词法分析的基础上将单词序列组合成语法树,如“程序”,“语句”,“表达式”等等.语法分析程序判断源程序在结构上是否正确。源程序的结构由上下文无关文法描述。

{
  "type": "Program",
  "start": 0,
  "end": 12,
  "body": [
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "age"
          },
          "init": {
            "type": "Literal",
            "value": "18",
            "raw": "'18'"
          }
        }
      ],
      "kind": "let"
    }
  ],
  "sourceType": "module"
}

常见的Javascript Parser有很多:

  • babylon:应用于bable
  • acorn:应用于webpack
  • espree:应用于eslint

四、拿babel为例

Babel是一个常用的工具,它的工作过程经过三个阶段,解析(parsing)、转换(transform)、生成(generate),如下图所示,在parse阶段,babel使用babylon库将源代码转换为AST,在transform阶段,利用各种插件进行代码转换,在generator阶段,再利用代码生成工具,将AST转换成代码。

一个简单的需求进行说明

我们想在代码中的console打印出来的内容前面加上它所在的函数名称,代码如下:

// index.js
function compile(code) {
   // todo
}
const code = `
    function foo(){
        console.log('bar')
    }
`
const result = compile(code)
console.log(result.code)

首先我们先安装bable的全家桶工具:

yarn add @babel/{parser,traverse,types,generator}

然后将其引入文件中:

const generator = require("@babel/generator")
const parser = require("@babel/parser")
const traverse = require("@babel/traverse")
const t = require("@babel/types") 
function compile(code) {
    //tode
}
const code = `
    function foo(){
        console.log('bar')
    }
`
const result = compile(code)
console.log(result.code)

我们可以通过AST Explorer查看code代码的抽象语法树结构,注意,这里面我们的解析工具要选用babylon7,这样和我们例子中代码解析出的结构才匹配

image.png

先解析拿到AST,直接生成代码片段:

const generator = require("@babel/generator")
const parser = require("@babel/parser")
const traverse = require("@babel/traverse")
const t = require("@babel/types")
function compile(code) {
    //   1. 解析
    const ast = parser.parse(code)
    //   2. 遍历
   
    //   3. 生成代码片段
    return generator.default(ast, {}, code)
}
const code = `
    function foo(){
        console.log('bar')
    }
`
const result = compile(code)
console.log(result.code)

运行一下

node index.js

输出结果
image.png
说明我们的代码没有问题,已经跑通了!剩下的只需要我们在第二阶段进行处理了。

第二阶段
需要使用到访问者(Visitors),访问者是一个用于 AST 遍历的跨语言的模式。 简单的说它们就是一个对象,定义了用于在一个树状结构中获取具体节点的方法。这么说有些抽象所以让我们来看一个例子。

const MyVisitor = {
  Identifier() {
    console.log("Called!");
  }
};

// 你也可以先创建一个访问者对象,并在稍后给它添加方法。
let visitor = {};
visitor.MemberExpression = function() {};
visitor.FunctionDeclaration = function() {}

这是一个简单的访问者,把它用于遍历中时,每当在树中遇见一个 Identifier 的时候会调用 Identifier() 方法。
所以在下面的代码中 Identifier() 方法会被调用四次(包括 square 在内,总共有四个 Identifier)。).

function square(n) {
  return n * n;
}

path.traverse(MyVisitor);
Called!
Called!
Called!
Called!

回到我们的例子,我们只需要创建一个访问者,访问到CallExpression节点,然后通过判断,去修改它arguments属性的参数就可以完成我们的任务了
image.png
修改我们的代码

const generator = require("@babel/generator")
const parser = require("@babel/parser")
const traverse = require("@babel/traverse")
const t = require("@babel/types")
function compile(code) {
    //   1. 解析
    const ast = parser.parse(code)
    //   2. 遍历
   		 //visitor可以对特定节点进行处理
    const visitor = {
      //定义需要转换的节点CallExpression
        CallExpression(path) {
          	//获取当前的节点
            const { callee } = path.node;
          	//判断
            if (
                t.isMemberExpression(callee)
                &&
                callee.object.name === 'console'
                &&
                callee.property.name === 'log'
            ) {
              	// 获取上层FunctionDeclaration路径
                const funcPath = path.findParent(p => {
                    return p.isFunctionDeclaration();
                })
               	// 将上层函数名添加到参数前
                path.node.arguments.unshift(
                    t.stringLiteral(`function name ${funcPath.node.id.name}:`)
                )
            }
        }
    }
    traverse.default(ast, visitor)
    //   3. 生成代码片段
    return generator.default(ast, {}, code)
}
const code = `
    function foo(){
        console.log('bar')
    }
`
const result = compile(code)
console.log(result.code)

我们再来打印下
image.png

这样我们就完成了整个任务,当然这只是一个很简单的例子,在实际开发中,我们还需要进行更复杂的判断才能保证我们的功能完善。

JS事件循环机制初探

作者:葉@毛豆前端

事件循环机制

js的事件循环机制,在我理解起来就是执行上下文,对函数的出栈和入栈的一个过程。都知道js的一大特点是单线程,这也是这个语言的核心特征。试想一下,如果js是个多线程的语言,同时存在两个线程,一个线程是在某个Dom上添加节点,而另一个线程却是删除节点,这个时候浏览器就会产生错乱,而我们关心的js事件循环机制,正是由js的单线程特质决定的。

先上图,看下大致流程

事件循环 在上图的过程中:

  1. js引擎逐句的执行script整体代码,形成执行栈
  2. 当执行遇到异步代码时,会指给对应的异步进程进行处理(WEB API)
  3. 等待异步任务有了运行结果,js的运行环境就在相应的”任务队列”之中push事件任务,等待着被js引擎执行,而当异步没有产生回调(callback)或者说是事件任务,那他就不会push到任务队列里面去。
  4. 执行栈任务执行完毕,便会查询任务队列,如果不为空,则读取一个任务推入主线程处理
  5. 重复第4个步骤,直到任务队列为空,这样过程即为事件循环,在这执行过程中如果有产生新的异步任务也会按照上述(3)的方式进行处理。在这个过程中step(1)同步环境执行,step(4)这样的循环过程即为事件循环

涉及的到异步进程:

  • DOM事件,是由浏览器的DOM模块处理,达到触发条件时,在任务队列中添加相应的回调函数
  • 定时器setTimeout等,是由浏览器的Timer模块处理,达到设置的时间点时,在任务队列中添加回调
  • ajax请求,是由浏览器的Network模块处理,等待请求返回,在任务队列中添加回调

知道了js的大致执行的过程了,我们看下其中涉及到的一些概念

栈(stack)又名堆栈,它是一种运算受限的线性表。其限制是仅允许在表的一端进行插入和删除运算

栈 知道了栈是一种后进先出的数据结构后,我们运行一下代码,并模拟它的入栈和出栈过程

function fun2() {
    console.log('fun2')
}
function fun1() {
    fun2();
}
setTimeout(() => {
	console.log('setTimeout')
})
fun1();

以上的出入栈如图所示

队列

任务队列

js中存在着多个任务队列,并且不同的任务队列之间优先级不同,优先级高的先被获取。同一队列中按照队列顺序被取用 任务队列分为两种类型

  • macrotask queue(宏任务队列):script(整体代码),setTimeout, setInterval,setImmediate,I/O,UI rendering
  • microtask queue(微任务队列):process.nextTick, Promises(这里指浏览器实现的原生 Promise), MutationObserver

两者的区别:

  • macrotask queue:存在多个,存在优先级
  • microtask queue:仅一个,按照队列顺序执行

事实上,事件循环的顺序,决定了JavaScript代码的执行顺序。执行时从script(整体代码)开始第一次循环,之后全局上下文进入函数调用栈,直到执行栈清空,然后开始执行所有的microtask,当所有可执行的micro-task执行完毕之后。循环再次从macro-task开始,找到其中一个任务队列执行完毕,然后再执行所有的micro-task,这样一直循环下去。事件循环每次只会入栈一个 macrotask ,主线程执行完该任务后又会先检查 microtasks 队列并完成里面的所有任务后再执行 macrotask。

我们举个例子验证下吧:


console.log('-----start-----')
setTimeout(()=>{
	console.log('setTimeout1-macroTask')
},1000)
Promise.resolve().then(() => {
    console.log('Promise1-microTask')
});
Promise.resolve().then(() => {
    console.log('Promise2-microTask')
});
setTimeout(()=>{
	console.log('setTimeout2-macroTask')
},100)
console.log('-----end-----')

输出结果
-----start-----
-----end-----
Promise1-microTask
Promise2-microTask
setTimeout2-microTask
setTimeout1-microTask

具体过程,我们稍微概括下:

  1. 遇到console.log,立即执行,输出“—–start—–”
  2. setTimeout,交给异步模块处理,执行完后,回调放入macrotask queue中
  3. 遇到Promise,执行then部分,回调放入microtask queue
  4. 遇到Promise,执行then部分,回调放入microtask queue,此时的microtask queue队列中就有两个事件了
  5. 还是遇到setTimeout,同样交给异步处理模块,执行完后,回调同样是一个macrotask任务,但因为setTimeout2的时间要比setTimeout1的短,因此先输出setTimeout2
  6. 再次遇到console.log,依旧立即执行,输出“—–end—–”
  7. 执行结束

Canvas简介

作者:较瘦@毛豆前端

1. cavans是什么

我们都知道HTML5新增了canvas元素,是HTML5的核心技术之一。canvas又被称为画布,我们可以使用canvas元素结合javascript来绘制各种图形,制作动画效果等

2. 基本用法

使用canvas元素绘制图形,需要以下三步

(1)获取canvas对象

(2)获取上下文对象context

(3)绘制图形

  1. <font color=green>绘制矩形</font>
       <template>
       <div>
             <canvas id="canvas" width='1200' height="1000"></canvas>
       </div>
       </template>
    
       // 矩形相关
       // 获取canvas对象
       let canvas = document.getElementById('canvas'); 
       // 获取上下文对象 context
       let context = canvas.getContext('2d');
       // 绘制矩形
       context.fillStyle = 'black'
       context.strokeStyle = 'blue'
       context.fillRect(0,0,100,100) //实心矩形 起始点的x坐标, 起始点的y坐标,矩形的宽,矩形的搞
       context.strokeRect(120, 0, 100, 100);// 空心矩形
    
    

说明:fillStyle 是context对象的一个属性,有三种取值(颜色值、渐变色、图案)

<font color=red>注意:对于canvas的宽度和高度,一定不要在css样式中定义,而是在html属性中定义(在css样式中定义,我们使用canvas对象获取的宽度和高度是默认值而不是实际的宽度和高度)。 另外:strokeStyle( fillStyle)属性的设置必须在strokeRect()或者fillRect()方法之前定义,否则属性设置无效</font>

  1. <font color=green>绘制线条</font>

    在canvas中,我们可以使用moveTo(x, y)和lineTo(x, y)这两个方法配合使用来画直线

    用法如下:

       //绘制线条
       context.strokeStyle = 'green'; // 画笔颜色
       context.moveTo(300,300); // 起点坐标
       context.lineTo(500, 500); // 终点坐标
       context.stroke(); // 绘制
    

    <font color=red>注意:canvas中使用的坐标系是W3C坐标系(y轴正方向是向下的)。如果要绘制多条线条:lineTo()这个放个是可以重复使用的,第一次lineTo()后,画笔将自动移动到终点坐标位置,第二次lineTo() 会以 “上一个终点坐标” 作为第二次调用的起点坐标,我们也可以借此画三角形、箭头等多边形</font>

  2. <font color=green>绘制圆形</font>

    我们可以使用arc(x, y, 半径, 开始角度, 结束角度, anticlockwise)方法来画一个圆

       // 画圆 在canvas中,绘制圆形或者圆弧时用到的是弧度
       context.beginPath();
       context.fillStyle = 'rgba(0,255,0,0.25)';
       context.arc(200, 220, 100, 0, Math.PI* 2, true);
       context.closePath();
       context.fill()
    

    说明:绘制圆形时需要调用beginPath()方法来声明开始,使用arc() 方法画圆完成后,还需要调用closePath() 来关闭当前路径,二者一般是配合使用的。

    arc() 方法参数说明:

    (1)x 、y 分别表示圆心横坐标和纵坐标

    (2)开始角度和结束角度都是以<font color=red>弧度</font>为单位。例如:180度 就是 Math.PI

    (3)anticlockwise 是一个布尔值,当为true时表示按逆时针绘制,为false时表示按顺时针方向绘制

  3. <font color=green>清空画布</font>

    在canvas中,我们可以使用clearRect(x, y, width, height) 方法来清空 ”指定矩形区域“

       // 清除矩形区域
       context.clearRect(50,50,120,120)
    
    

    说明:x和y 分别表示清空矩形区域最左上角的坐标, width表示矩形的宽度、height表示矩形的高度。

    <font color=red>如果要清空整个canvas,则width 和  height分别为 canvas画布的宽度和高度</font>

3. 图片操作

canvas不仅可以绘制各种形状的图形,还可以将图片导入canvas中进行各种操作。例如:平铺、切割、像素处理等

  1. <font color=green>绘制图片</font>

    我们可以使用drawImage(image, dx, dy, dw, dh) 方法来绘制图片。

    参数说明:image:表示页面中的图片

    ​ dx:表示图片左上角的横坐标

    ​ dy:表示图片左上角的纵坐标

    ​ dw:表示图片的宽度

    ​ dh:表示图片的高度

       // 画圆 在canvas中,绘制圆形或者圆弧时用到的是弧度
       context.beginPath();
       context.fillStyle = 'rgba(0,255,0,0.25)';
       context.arc(200, 220, 100, 0, Math.PI* 2, true);
       context.closePath();
       context.fill()
       // 使用clip()方法,使得切割区域为上面绘制的圆形
       context.clip()
       let image = new Image();
       image.src = ImgSrc
       image.onload = function () {
             console.log('>>>>>>>>')
             context.drawImage(image, 0, 100)
       }
    

    <font color=red>注意:必须在图片载入完成后才能将图片绘制到canvas上,如果图片没有载入完成就使用drawImage() 方法进行绘制的话,canvas将不会显示图片</font>

  2. <font color=green>切割图片</font>

    在canvas中,我们可以利用clip() 方法来切割绘制的图片

    使用方法:

    1. 绘制基本图形

    2. 使用clip() 方法

    3. 绘制图片

      <font color=green>我们可以使用矩形、多边形、圆形等作为切割区域来切割图片</font>

    <font size=2>本次主要介绍canvas的基础部分,canvas还有很多高阶应用。例如:边界检测、高级动画、canvas图表库….. 后续学习会继续分享给大家</font>

浏览器缓存机制探索

作者:小麻姐@毛豆前端

浏览器缓存

首先用一张图了解下缓存:

结果

为什么要缓存

我们从性能方面分析下为什么要缓存

缓存的作用

缓存能够对已有的资源进行重用,减少延迟和网络阻塞,达到减少某个资源展示的时间。 其实我们对于页面静态资源的要求就两点

1、静态资源加载速度

2、页面渲染速度

页面渲染速度建立在资源加载速度之上,但不同资源类型的加载顺序和时机也会对其产生影响,所以缓存的可操作空间非常大

缓存的应用场景

1、每次都加载某个同样的静态文件 => 浪费带宽,重复请求 => 让浏览器使用本地缓存(协商缓存,返回304)

2、协商缓存还是要和服务器通信 => 依然存在有网络请求 => 强制浏览器使用本地强缓存(返回200)

3、缓存要更新啊,兄弟,网络请求都没了,我咋知道啥时候要更新?=> 让请求(header加上ETag)或者url的修改与文件内容关联(文件名加哈希值)

4、CTO大佬说,我们买了阿里还是腾讯的CDN,几百G呢,用起来啊 => 把静态资源和动态网页分集群部署,静态资源部署到CDN节点上,网页中引用的资源变成对应的部署路径 => html中的资源引用和CDN上的静态资源对应的url地址联系起来了 => 问题来了,更新的时候先上线页面,还是先上线静态资源?(蠢,等到半天三四点啊,用户都睡了,随便你先上哪个)

5、老板说:我们的产品将来是国际化的,不存在所谓的半夜三点 => GG,咋办?=> 用非覆盖式发布啊,用文件的摘要信息来对资源文件进行重命名,把摘要信息放到资源文件发布路径中,这样,内容有修改的资源就变成了一个新的文件发布到线上,不会覆盖已有的资源文件。上线过程中,先全量部署静态资源,再灰度部署页面 (参考网上内容)

各类缓存

数据缓存

1、cookie: 一个用户所请求的操作属于一个会话,而另一个用户所请求的是另一个会话,由于http是无状态的,一旦会话完毕就会关闭,这样无法通过会话保存所请求的内容。 一个用户请求服务器,服务器如需保存用户的信息,则返回的response加cookie信息,浏览器会将cookie保存在浏览器上,下次请求会将请求信息和cookie一起发给浏览器,这样服务器通过cookie信息识别用户信息,甚至修改cookie信息,以此来辨认用户状态。

2、session: 和cookie不同的是,session是保存在服务器上的。服务器通过识别cookie带来的sessin-id,来找到服务器上保存的用户信息,以此来识别用户。

当程序需要为某个浏览器创建session信息的话: 1、程序首先检查该请求是否携带sessionId.

2、携带sessionId的话程序会自己在服务器上查询该sessionId,拿到session信息。如果查询不到,会新建一个一个sessionId

3、请求未携带sessionId,程序会在附服务器上创建一个session,将此session的sessionId返回给浏览器。

缺点:在请求头上,大小4k。

常用的配置属性有以下几个

Expires :cookie最长有效期

Max-Age:在 cookie 失效之前需要经过的秒数。(当Expires和Max-Age同时存在时,文档中给出的是已Max-Age为准,可是我自己用Chrome实验的结果是取二者中最长有效期的值)

Domain:指定 cookie 可以送达的主机名。

Path:指定一个 URL 路径,这个路径必须出现在要请求的资源的路径中才可以发送 Cookie 首部

Secure:一个带有安全属性的 cookie 只有在请求使用SSL和HTTPS协议的时候才会被发送到服务器。

HttpOnly:设置了 HttpOnly 属性的 cookie 不能使用 JavaScript 经由 Document.cookie 属性、XMLHttpRequest 和 Request APIs 进行访问,以防范跨站脚本攻击(XSS)。

storage

1、sessionStorage:

sessionStorage存储在浏览器上,存储内容可以是任何形式(包括:数组、图片、json、样式。。。)等。明文存储,所以一般不会保存较敏感信息。当页面关闭的时候不会保存。

2、localStorage:

同sessionStorage一样,也是存储在浏览器上,存储内容形式多样,明文存储。但是当浏览器关闭的时候,信息仍然会存在。除非人为删除,否则理论上永远存在。

两者存储大小5M左右。

监听storage事件 可以通过监听 window 对象的 storage 事件并指定其事件处理函数,当页面中对 localStorage 或 sessionStorage 进行修改时,则会触发对应的处理函数

window.addEventListener('storage',function(e){
   console.log('key='+e.key+',oldValue='+e.oldValue+',newValue='+e.newValue);
})

触发事件的时间对象(e 参数值)有几个属性:

key : 键值。

oldValue : 被修改前的值。

newValue : 被修改后的值。

url : 页面url。

storageArea : 被修改的 storage 对象。

HTTP Header缓存

良好的缓存能降低资源重复加载提升页面加载速度。 缓存分为强缓存和协商缓存

一、基本概述

1、请求原理

1)、浏览器加载资源的时候首先检查请求头的expires和cache-control来判断有没有强缓存,有的话直接使用强缓存。

2)、如果没有命中强缓存,则向服务器发送请求,根据last-modified和etag来检查是否有协商缓存,有的话返回304,直接使用协商缓存。

3)、未命中协商缓存的话则直接从服务器上拉取资源。

2、相同点

命中后都是从浏览器中直接拉取资源。

3、不同点

强缓存不发送请求到服务器,协商缓存则发请求到服务器。

二、强缓存

强缓存主要通过expier和cache-control来表示缓存时间和资源。

1、expire

expire是http1.0 提供的规范。它的值是一个GMT格式的绝对时间值。这代表expire失效的时间,在这个时间之前永远有效。但是这个值是受到浏览器上本地时间影响的,如果浏览器和服务器时间不一致,则会导致缓存混乱。当cache-control: max-age和expire同时存在的时候,max-age优先级高。

2、cache-control

cache-control是http1.1提供的规范。利用max-age来判断缓存时间,是一个相对值。例如cache-control: max-age=1000,则表示缓存在1000秒内有效。

cache-control常用的值:

no-cache:不使用本地缓存。需要使用缓存协商,先与服务器确认返回的响应是否被更改,如果之前的响应中存在ETag,那么请求的时候会与服务端验证,如果资源未被更改,则可以避免重新下载。

no-store:直接禁止游览器缓存数据,每次用户请求该资源,都会向服务器发送一个请求,每次都会下载完整的资源。

public:可以被所有的用户缓存,包括终端用户和CDN等中间代理服务器。

private:只能被终端用户的浏览器缓存,不允许CDN等中继缓存服务器对其缓存。

Cache-Control与Expires可以在服务端配置同时启用,同时启用的时候Cache-Control优先级高。

二、协商缓存

协商缓存就是由服务器来判断缓存是否有效,浏览器和服务器之间协商一个标示,根据该标示来判断是否使用缓存。 这个主要涉及到两组header字段:Etag和If-None-Match、Last-Modified和If-Modified-Since

1、Etag和If-None-Match Etag/If-None-Match返回的是一个校验码。ETag可以保证每一个资源是唯一的,资源变化都会导致ETag变化。服务器根据浏览器上送的If-None-Match值来判断是否命中缓存。

与Last-Modified不一样的是,当服务器返回304 Not Modified的响应时,由于ETag重新生成过,response header中还会把这个ETag返回,即使这个ETag跟之前的没有变化。

2、Last-Modify/If-Modify-Since 浏览器第一次请求一个资源的时候,服务器返回的header中会加上Last-Modify,Last-modify是一个时间标识该资源的最后修改时间。

当浏览器再次请求该资源时,request的请求头中会包含If-Modify-Since,该值为缓存之前返回的Last-Modify。服务器收到If-Modify-Since后,根据资源的最后修改时间判断是否命中缓存。

如果命中缓存,则返回304,并且不会返回资源内容,并且不会返回Last-Modify。

三、缓存流程图

流程 其余缓存内容,下次在和大家一起探讨。

Promise解读

作者:胡籽@毛豆前端

Promise Introduction

首先让我们来了解一下什么是Promise。

Promise是抽象异步处理对象以及对其进行各种操作的组件。

Promise对象有以下两个特点:

(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:Pending(进行中)、Resolved/Fulfilled(已完成)、Rejected(已失败)。

只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都是无法改变这个状态。

(2)一旦状态改变,就不会变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从Pending变为Resolved或者从Pending变为Rejected

只要这两种情况发生,状态就凝固了,不会更改,一直保持结果。就算改变已发生,再对Promise对象添加回调函数,也可立即得到这个结果。

Promise对象可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数,还提供了统一的接口,更易控制异步操作。

Promise存在的缺点:

(1) 一旦新建就会立即执行,无法中途取消;

(2) 如不设置回调函数,Promise内部抛出的错误,不会反应到外部;

(3) 当处于Pending状态,无法得知目前进展到哪个阶段(是刚开始还是即将完成)


Promise API

Constructor

Promise 类似 XMLHttpRequest,从构造函数Promise来创建新promise对象作为接口。 可使用new 来调用 Promise 的构造器进行实例化,创建 promise 对象。

let promise = new Promise((resolve, reject) => {
    // 异步处理
    // 处理结束后,调用resolve 或 reject
})

Instance Method

可通过promise.then()实例方法,为通过new生成的promise对象设置其值在resolve(成功)reject(失败) 时调用的回调函数。

promise.then(onFulfilled, onRejected)

onFulfilledonRejected 两个都为可选参数。

只对异常情况做处理,可使用promise.then(undefined,onRejected), 只指定reject时的回调函数即可。异常处理promise.catch(onRejected)可为更好的选择。

Static Method

Promise包括几个常用的静态方法:Promise.resolve()Promise.reject()Promise.all()Promise.race()

首先看一下Promise workflow

Promise工作流

示例代码:

function asyncFunc() {
    //new Promise之后,返回一个promise对象
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('Hello Promise!')
        }, 10)
    })
}

//为promise对象设置.then() 调用返回值时的回调函数
asyncFunc().then((value) => {
    console.log(value);
}).catch((error) => {
    console.log(error);
})

asyncFunc()返回的promise对象会在setTimeout之后的10ms时被resolve,此时then 的回调函数会被调用,并输出Hello Promise!。当前情况下,catch 的回调函数不会被执行(因为promise返回了resolve)。


编写Promise代码

创建promise对象的流程如下:

(1) new Promise(fn) 返回一个promise对象

(2) 在 fn 中指定异步操作

处理结果正常:调用`resolve(处理结果值) 处理结果错误:调用reject(Error 对象)

接下来,按照上述流程用Promise来通过异步处理方式获取XMLHttpRequest(XHR)的数据。 创建一个Promise把XHR处理包装好的名为 getUrl 的函数

function getURL(URL) {
    return new Promise((resolve, reject) => {
        let req = new XMLHttpRequest();
        req.open('GET', URL, true);
        req.onload = () => {
            if (req.status === 200) {
                resolve(req.responseText);
            } else {
                reject(new Error(req.statusText));
            }
        };
        req.onerror = () => {
            reject(new Error(req.statusText));
        };
        req.send();
    });
}

// 运行示例
let url = "http://httpbin.org/get";
getURL(url).then((value) => {
    console.log(value);
}).catch((error) => {
    console.error(error);
});

getURL 只有通过XHR取得结果状态为200时调用 resolve (数据获取成功时), 其他情况则调用 reject 方法。


Promise源码

Promise.resolve

静态方法Promise.resolve 有:Promise.resolve(value)Promise.resolve(promise)Promise.resolve(theanable) 这三种形式。 都会产生一个新的 Promise , 从而可以继续 then 链式调用。

Promise.resolve = function (value) {
    return new Promise(function (resolve) {
        resolve(value);
    });
};

Promise.resolve 的实现就是new一个新的Promise实例并调用resolve方法,最后返回。

Example:

Promise.resolve('hello').then((value) => {
    console.log(value); // 'hello'
}, (value) => {
    // no called
})

var p = Promise.resolve([1, 2, 3]);
p.then((val) => {
    console.log(val); //[1, 2, 3]
})

var old = Promise.resolve(true);
var new = Promise.resolve(old);
new.then((val) => {
    console.log(val); // true
})

Promise.reject

Promise.rejectPromise.resolve 同理,只是替换成reject。

Promise.reject = function(value) {
    return new Promise(function(resolve, reject) {
        reject(value);
    })
}

Promise.all

Promise.all 方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

实现的思路就是类似与用一个计数器,从零开始,每当一个 Promise 实例调用了 resolve ,则 +1。当计数器等于 Promise 实例的数量时,表示全部执行完,此时返回结果。

Promise.all = function (arr) {
    var args = Array.prototype.slice.call(arr);
    
    return new Promise(function (resolve, reject) {
        if(args.length === 0) return resolve([]);
        var remaining = args.length;
        
        function res(i, val) {
            if(val && (typeof val === 'object' || typeof val === 'function')){
                var then = val.then;
                if(typeof then === 'function'){
                    var p = new Promise(then.bind(val));
                    p.then(function(val) {
                        res(i, val);
                    }, reject)
                    return;
                }
                
                args[i] = val;
                if(--remaining === 0){
                    resolve(args);
                }
            }
        }
        for (var i = 0; i < args.length; i++) {
            res(i, args[i]);
        }
    })
}

源码中是做减法,跟计数器做加法的思路是一致的。new 一个新的 Promise,当触发了计数器设定的值(即 0),则调用它的 resolve,从而触发 then 函数。

res 函数里,给每一个 Promise 实例绑定一个 then 方法,当触发 resolve,即触发 then,从而再次调用 res 函数。当传入的值不再是 Promise 实例,就用 args 记录,作为返回的结果数组。并重新设置计数器 remaining(做减法)。

当 remaining 被减到 0,表示所有传入的 Promise 实例都执行了 resolve,此时可以调用新 new 出来的 Promise 实例的 resolve 。

Example:

var promise = Promise.resolve(3);
Promise.all([true, promise]).then(values => {
    console.log(values); // [true, 3]
});

Promise.race

Promise.race 方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。 与 Promise.all 不同的是:只要有一个 promise 对象进入 FulFilled 或者 Rejected 状态的话,就会继续进行后面的处理。

Promise.race = function (values) {
    return new Promise(function (resolve, reject) {
        values.forEach(function (value) {
            Promise.resolve(value).then(resolve, reject);
        })
    })
}

最后

把Promise 从头捋了一遍,发现源码并非想象中的那般难。。。。