Oliver

Oliver 查看完整档案

北京编辑北京邮电大学  |  计算机科学与技术 编辑  |  填写所在公司/组织填写个人主网站
编辑
  1. Slow Done, Achieve More.
  2. Don't Repeat Yourself.
  3. always be patient, always be enthusiastic.

个人动态

Oliver 发布了文章 · 11月21日

React:useState的异步更新

引入

function App() {
  const [n, setN] = useState(0)
  const onClick = ()=>{
    setN(n+1)
    setN(n+1) //  此时发现,n只能+1,而不会+2
    // setN(i=>i+1)
    // setN(i=>i+1)
  }
  return (
    <div className="App">
      <h1>n: {n}</h1>
      <button onClick={onClick}>+2</button>
    </div>
  );
}

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

react代码如上图:

  • 直觉上当我们点击button,应该会执行两次setN,n变为2。
  • 实际上:n变为了1

为什么n是1,而不是2?

首先要搞清楚useState的原理,可以参考我的博客,讲解了useState的简易实现:https://segmentfault.com/a/11...

  • 我们知道:

    1. useState每次执行会返回一个新的state(简单类型的等值拷贝)
    2. setState会触发UI更新(重新render,执行函数组件)
    3. 由于UI更新是异步任务,所以setState也是一个异步过程

当我们两次setN(n+1)时候,实际上形成了两个闭包,都保存了对此时n的状态(n=0)的引用。
在setN后:

  1. 先分别生成了两个新的n,数值上都等于n+1(即1),但彼此无关。
  2. 分别进行了render,而只有最新一次render有效,此次render引用了最后一次setN函数里生成的n。

解决方法

// 利用函数,接收旧值,进行更新
setState( x => x+1 )
  • 接收的函数 x=>x+1 并未保持对n的引用,而是表达了一种 加1 操作
  • 推荐使用函数代码进行 setState
查看原文

赞 0 收藏 0 评论 0

Oliver 发布了文章 · 11月21日

项目记录 : goodpic - 图床工具

goodpic

预览链接:https://oliver-ysq.github.io/...
代码地址:https://github.com/Oliver-Ysq...

技术栈:

  • React:base技术框架
  • React-Router:路由管理
  • Mobx6:全局数据管理
  • styled-component:css in js解决方案
  • antd:组件库
  • leancloud:阿里serverless解决方案

实现功能:

  1. 支持用户的注册和登录
  2. 支持 png/jpg/gif/svg 格式的、大小在1MB以内的图片文件的上传
  3. 支持图库的管理(包括查看和删除)
  4. 图库支持动态(分步)滑动加载。
  5. 使用@media作移动端适配。

项目记录

  1. 懒加载
    按需加载组件,防止一次性加载全部组件带来的卡顿。

    import {Suspense, lazy} from "react";
    ...
    const Home = lazy(()=>import("./pages/component/Home"))
    const About = lazy(()=>import("./pages/component/About"))
    
    jsx:
    <Suspense fallback={<Loading />}>   {/* 异步加载 */}
        <Switch>
            <Route path='/' exact component={Home}/>
            <Route path='/about' exact component={About}/>
            <Route path='/history' exact component={History}/>
        </Switch>
    </Suspense>
  2. styled-components

    • 基本使用
    import styled from "styled-components"
    const MyHeader = styled.header` //生成一个带有如下样式的<header></header>标签
        color: red;
    `
    
    export default function component(){
        return(
            <MyHeader>welcome</MyHeader>
        )
    }
    • 为已存在的组件增加css样式:
    const Button = styled.button`
        font-size: 24px;
    `
    
    const BigButton = style(Button)`
        font-size: 48px;
    `
    • 获取props
     const Button = styled.button`
         background: ${props => props.primary ? 'palevioletred' : 'white'};
         color: ${props => props.primary ? 'white' : 'palevioletred'};
         font-size: 1em;
     `
     render(
         <div>
             <Button>Normal</Button>
             <Button primary>Primary</Button>
         </div>
     );
  3. react-router使用
    关键点:

    • Router:BrowseRouter;HashRouter
      包裹router的显示范围
    • Route:
      每个可通过router跳转的页面为一个Route
    • Switch
      包裹Route
    • Link:Link;NavLink
      跳转到Route
  4. mobx
    三步走:
    (1)创建仓库

    import {makeAutoObservable} from 'mobx'
    class myStore{
        count: 0;
        add(){
            this.count++
        }   
    }
    export {myStore}

    (2)使用Context注入仓库

    import React, {createContext, useContext} from 'react'
    import {myStore} from './myStore'
    
    const context = createContext({
        myStore: new myStore()
    })
    
    export const useStores = () => useContext(context)

    (3)在组件中使用

    const {myStore} = useStores()
    //可以使用myStore中的方法
    myStore.count
    myStore.add()
  5. useRef

    const count = useRef(0)
    console.log(count.current)
  6. 非受控表单

    • useRef()方案
  7. 定制图片宽高

    • 若图片的url为imgurl,则定制宽为100,高为200的图片:

      imgurl/w/100/h/200
查看原文

赞 0 收藏 0 评论 0

Oliver 发布了文章 · 11月14日

React的事件绑定为什么要bind this

引入:绑定事件,我们一般怎么写?

在react开发中绑定事件,我们往往用这种方式:
image.png
此时我们发现,this指向了组件实例,符合我们的预期。

去掉bind this会如何?

image.png
可以看到,this指向了window

为什么去掉bind this会这样?

首先要知道,JSX语法实际上是createElement的语法糖

<div>Hello, { this.props.name }</div>
等价于
React.createElement( ‘div’,null, `Hello,${this.props.name}` )

createElement伪代码

function createElement(dom, params) {
  var domObj = document.createElement(dom);
  domObj.onclick = params.onclick;
  domObj.innerHTML = params.conent;
  return domObj
}

可以看到,我们的自定义组件类中onclick绑定的事件,是作为回调函数绑定到domObj.onclick上的。

onclick事件触发

button被点击时,会由React作为中介调用回调函数,此时的this指向丢失,就指向了window

bind this的原理

new关键字

在使用new关键字时,构造函数(即class)中的this都会强制赋值为新的对象。

使用bind将this的值绑定在函数中

除了bind this,还可以使用箭头函数方式

image.png

为什么箭头函数方式不需要bind this

  • 箭头函数内没有this,默认用父级作用域的this。
  • 当使用new关键字时,this指向新对象,同时箭头函数中的this也被赋值为了新对象且永远不会更改指向。
  • 等价于如下形式
//在构造函数内:
let _this = this
funtion fn(){
    console.log(_this)
}
查看原文

赞 0 收藏 0 评论 0

Oliver 收藏了文章 · 11月8日

JavaScript — 理解prototype与class

一步一步捋清prototype

前言

  • 学习JavaScript,不可避免地要学习JS中的面向对象的编程模式,但JS中的OOP和c++,java等典型的面向对象语言明显不同,“原型链”,es6中的“class”,都彰显着js中面向对象的与众不同。
  • 究竟什么是原型prototype呢?如何在JavaScript中实现“类”呢?

一张图

图片描述

图片分析

这是我总结的一张图片,简单的描绘了原型和构造函数、实例间的复杂关系。

  1. 全局对象window属性Object(此处仅用来举例,其余属性暂且不提),指向Object()这个函数对象
  2. Object中存着prototype,它指向了原型(原型中包含了对象的所有方法和属性)
  3. 当我们声明一个实例let obj = new Object(), 你可以发现它天生就带着__proto__属性。而__proto__竟然和Object.prototype指向了同一个对象。我们知道,它就是原型
    clipboard.png

一些思考

1.Object.prototype(即对象的原型)的原型是什么呢?

  • 我们不妨打印出来试一试:clipboard.png
    应该和你想的一样,对象的原型就是原型链的尽头了,它只是一个普通的对象,并没有原型。

2.Object.prototype中的constructor是什么?有什么用?

  • 你应该还记得我们刚刚像这样let obj = new Object();创建了一个对象实例obj,那么再来看看这个,你应该就一目了然了
    clipboard.png
    没错,constructor存在的意义就是为了让实例化地对象找到它的构造函数。(对实例化的数组,constructor指向Array;对实例化地函数,constructor指向Function)

3.但你不能人为constructor指向的一定是构造出当前对象的函数。

  • 例如,你能说出Object.prototype.constructor指向谁吗?clipboard.png
    你看,这个结果是不是很让人头大??Object.prototype指向了原型,而原型的constructor的又指回了Object。实际上这种试验是无意义的,因为我们要知道,constructor是提供给实例,用来定位它的构造函数的属性,而不是给原型本身使用的。
  • 而这种Object.prototype.constructor ===Object的现象,其实源于人为设计。为了让实例的对象都继承原型中的constructor,无法避免出现这种现象。

4.我们尝试更深入一些?尝试剖析一下数组对象的相关结构。

  • 问题1:Array.prototype.__proto__===?

    • 直接来看结果吧clipboard.png
    • 为什么会有这种结果呢?其实我们只需要理解清Array.prototype的本质就好了,其实Array.prototype只是一个Object()构造出的对象实例而已,一个对象实例的原型当然是Object.prototype啊!换句话说Array.prototype和obj在本质上是一样的!
  • 问题2:let arr=[1,2,3]; clipboard.png 属于对象的方法hasOwnProperty是如何在数组实例上生效的呢?

    1. arr在自身寻找hasOwnProperty方法,发现没有。
    2. arr又到他爸爸arr.__proto__即Array.prototype中寻找hasOwnProperty方法,仍然找不到 =
    3. arr再到他爷爷arr.__proto__.__proto__即Object.prototype中寻找,找到了这个方法,于是arr调用了这个方法。

类和继承

  1. class的默认属性

    class Person {
       name = 'Oliver'
       sayHi = ()=>{ console.log(hi) }
    }

    在class声明中
    通过 = 赋值的属性,会变成该class实例的属性默认值;
    通过箭头函数声明的方法,会挂载到该实例身上,而非class身上。

  2. 方法的重写

    // Person定义同上
    class Child extends Person{
       sayHi(){
           console.log('I am a child.')
           super.sayHi()
       }
    }

    子类重写父类的方法后,如果还想调用父类的方法,需要使用 super 关键字,super上挂载有父类的原始方法。

  3. class vs prototype

    // prototype方法
       function Person( name, age) {
              this.name = name,
               this.age = age
       }
       Person.prototype.sayHi = function() {
           console.log('你好,我叫'+this.name);
       }
       
       //class方法
    class Person{
       constructor(name,age){
           this.name = name;
           this.age = age;
       }
       sayHi(){ console.log('你好,我叫'+this.name); }
    }
  4. 继承

    class Children extends Person{
        constructor(name,age,grade){
            super(name,age);
            this.grade = grade;
        }
        sayGrade(){
            console.log('我今年${grade}年级');
        }
    }

conclusion

  • class只是prototype方法的语法糖而已,本质上没有区别
  • 运用顺序:理解prototype => 熟练使用prototype => 无压力使用class
查看原文

Oliver 发布了文章 · 3月20日

浏览器渲染机制

  1. 浏览器渲染机制

    1. style样式渲染:

      1. style标签中的样式由html解析器进行解析;
      2. style标签内的样式是异步解析的(分组解析渲染,容易出现闪屏现象)
    2. link样式渲染:

      1. link引入的样式由stylesheet解析器解析
      2. 同步解析(css解析器会阻塞页面的渲染,但不干扰解析过程)
      3. 推荐使用link引入样式
    3. js对渲染的影响:

      JS会阻塞渲染

  2. 浏览器的渲染过程

    • 请求(HTML request)并接收到HTML文件(receive Response;receive Data),解析转化为DOM树
    • 将CSS文件转换为CSSOM样式树

      在这一过程中,浏览器会确定下每一个节点的样式到底是什么,并且这一过程其实是很消耗资源的。

    • 遇到 script 标签时调用JavaScript引擎 执行JS代码,绑定事件,修改 dom 树和 css 树
    • 生成渲染树(依赖GPU)

      渲染树只会包括需要显示的节点和这些节点的样式信息。

      比如说,如果某个节点是 display: none 的,那么就不会在渲染树中显示。

    • 会根据渲染树来进行布局(也可以叫做回流);然后调用 GPU 绘制(paint),合成图层(composite),显示在屏幕上。

image.png

  1. 阻塞

    1. css阻塞:
      1. style标签:

        • 由html解析
        • 不阻塞浏览器渲染
        • 不阻塞 DOM 解析
      2. link引入css:(推荐使用)
        • 由 css 解析器进行解析
        • 阻塞浏览器渲染
        • 阻塞 script 标签中 js 执行
        • 不阻塞 DOM 解析
      3. 优化的核心理念:尽可能提高外部 css 加载速度

        • 优化 css 代码
        • 通过合并css文件,减少http请求数
        • 对css进行压缩
    2. JS阻塞
      1. 阻塞后续DOM解析
      2. 阻塞渲染
      3. 阻塞后续JS执行
  2. 图层

    浏览器渲染DOM所做的实际工作是:

    1. 获取dom,分割成多个图层
    2. recalculate style :重新计算每个图层样式
    3. layout:回流(生成布局)
    4. paint:重绘(绘制图形)
    5. composite Layers:组合多个图层,呈现在页面上
  3. 回流(重排)和重绘

    • 回流:布局或者几何属性需要改变就称为回流。
    • 重绘:当节点需要更改外观而不会影响布局的,比如改变 color 就叫称为重绘

      可能触发重绘的操作:

      • 浏览器窗口大小改变
      • dom 尺寸、位置、内容发生改变
      • 元素字体大小变化
      • 添加或者删除可见的 dom 元素
      • 查询某些属性时(例如clientWidth)
    • 回流必定会发生重绘,重绘不一定会引发回流。回流所需的成本比重绘高的多。
    • 优化方法:

      1. js:

        • 尽量使用class进行样式修改
        • 减少dom的增删次数,可使用 字符串 或者 documentFragment 一次性插入
      2. css:

        • 避免使用table布局
        • 将动画效果应用到position属性为absolutefixed的元素上
查看原文

赞 0 收藏 0 评论 0

Oliver 发布了文章 · 3月8日

深入JS - 执行上下文、词法环境、闭包其实很简单

作用域 === 词法环境对象

作用域链 === 词法环境利用outer串联的词法环境链

(一)运行代码过程分为三步:

1. 初始化上下文
2. 注册变量
3. 执行代码

(二)Javascript 程序执行过程

1. 全局上下文压入调用栈

callStack = [globalContext]
 // globalContext:
 // { Var-Environment, Lex-Environment, this }
 // 上下文包含内容:变量环境,词法环境,this指向

2. 运行全局上下文代码:

1) 初始化上下文:生成一个Global Environment 全局词法环境

        Global-Environment = {  
            record: Global-Map  
            outer: null  
        }
        // 一般情况下变量环境和词法作用域指向同一个对象
        globalContext.Var-Environment = Global-Environment
        globalContext.Lex-Environment = Global-Environment
  • 该词法环境的数据记录在 Global-Map 中:
    var,function变量,注册在变量环境
    let,const变量,注册在词法环境
    两者中的变量,汇总在全局词法环境的record中
  • 该词法环境的外部环境 outer 为 null(全局词法环境为作用域链的终点

2) 注册 var 变量和 function 函数:变量提升

var a = 1  
...
function fn(){}
...
  • a 注册并初始化为 undefined
  Global-Map = {
    a: undefined
  }
  • fn 注册并生成一个 fn对象,内置属性 [[scope]] 指向创建fn时的词法环境,用于记录fn有权访问的词法环境。
    Global-Map = {
      a: undefined,
      fn: fn-Object
    }
    fn-Object = {  
      [[scope]]: Global-Environment  
    }

3) {}遇到let/const:

在代码块{}中使用let/const声明变量会使得该代码块形成块级作用域。

{
    let a = 1;
    var b = 2;
}
console.log(b)

此时,当前执行上下文的Lex-Environment不再指向原先的词法环境对象,而是指向一个重新创建的词法环境对象。该对象中outer为原先的词法环境对象。

    new-Environment = {
        Record: { ... }
        outer: Global-Environment
    }

要注意的是,在此块级作用域内使用var和function声明的变量注册在当前执行上下文的变量环境中;而let和const声明的变量注册在new-Evironment中。

4) 运行其他代码:

  • 若遇到赋值表达式,则进行赋值
    例如 a = 2

    Global-Map = {
      a: 2,
      fn: fn-Object
    }
  • 若遇到函数调用则压入调用栈

3. 函数上下文压入调用栈

callStack= [globalContext,fnContext]

4. 运行函数上下文代码:

1) 初始化上下文:生成一个 fn-Environment 词法环境

fn-Environment = {  
   record: fn-Map  
   outer: fn-Object.[[scope]]  
}  
fn-Map = {}
  • 初始化一个词法记录表fn-Map。
  • 词法环境中,outer 指向了该函数注册的对象 fn-Object 中的内置属性 [[scope]]

2) 注册 var 变量和 function 函数:

过程同上

3) 运行其他代码:

过程同上

4) 查找变量过程:

若在当前词法环境中找不到引用的变量,则沿outer向上查找。这就是作用域链

5) 运行完毕后出栈,该函数词法环境被垃圾回收机制清除干净。

callStack = [globalContext]

(三)使用闭包保存词法环境

  • 函数上下文在函数执行后就弹出调用栈了,那么如何保存函数的词法环境呢?
  • 答案是:用一个全局变量引用该词法环境,该词法环境就不会被垃圾回收机制清除了。

首先在 fn 中创建一个新函数并生成一个 newFn-Object 对象,内置属性 [[scope]] 指向创建fn时的词法环境(即 fn-environment ),这样失联的词法环境就被 newFn-Object记录下来了

    fn-Map = {  
       newFn: newFn-Object  
    }  
    newFn-Object = {  
       [[scope]]: fn-environment  
    }

现在,虽然可以通过新函数对象newFn-Object的[[scope]]属性访问 "失联"的词法环境,但由于外部函数运行上下文fn-Environment的退出,newFn-Object也处于“失联”状态,我们在全局无法访问内部函数。

其实很直观的,可以把内部函数赋值给一个全局变量,就可以访问外部函数那些“失联的词法环境”了。

  // fn  
  fn = function (){  
    var x = 1  
    return function(){  
     return x  
    }  
  }  
  //global  
  const getX = fn()

这样保证了 getX 对 newFn-Object 的引用,继而将 fn 的词法作用域保存了下来。

查看原文

赞 1 收藏 1 评论 0

Oliver 发布了文章 · 3月8日

【转】JS词法环境 - 用一个实例讲清楚词法环境和执行上下文

函数调用过程分析

var a = 2;

function foo() {
    console.log(a)
}

function bar(){
    var a = 5;
    foo()
}

bar()   //2

我们通过分析函数调用过程,来看看,为什么foo() 引用的是全局的a而不是bar里的a。

全局代码运行

  1. 全局运行上下文初始化:

    看起来是这样的:

  2. var声明和函数声明扫描scan:

    • 扫描var 声明:“var a = 2;”
    • 扫描到函数声明:“function foo() {console.log(a)}”
    • 扫描到函数声明:"function bar(){ var a = 5;foo()}"


      这时候整个环境看起来是这样的:

  3. 执行语句
  • 执行语句“a = 2;”

  • 执行调用语句:bar()

    bar()运行以后,上述讲到,会携带undefined作为thisArg,开始进入函数代码的运行。

进入函数代码

函数代码的执行和global的执行类似,也遵循我们的运行模型:

运行模型

运行代码 = 运行上下文初始化 + var声明和函数声明扫描scan + 执行语句;
  1. 初始化函数的运行上下:

    • 创建一个新的词法环境(Lexical Enviroment):localEnviroment

      • 使localEnviroment的outer为函数的'先天作用域'----函数对象的[[scope]]的值。
    • 创建一个新的运行上下文(Execution Context): barExecutionContext
    • 使得barExecutionContextt的LexicalEnvironment和VariableEnvironment 为localEnviroment
    • 判断携带进来的thisArg的值:

      • 如果是strict,使barExecutionContext.ThisBinding = thisArg;
      • 不是strict

        *   如果thisArg是undefined,使barExecutionContext.ThisBinding = globalobject;
        *   如果thisArg不是undefined,使barExecutionContext.ThisBinding = toObject(thisArg);
        
    • 这时整个环境看起来是这样的:

      • 整个过程简化来来是说:用函数自身创建时候携带的词法环境为“父”,创建一个函数自己的词法环境。
      • 图中虚线的意思,就是outer的实际的指向。函数运行时候的词法环境的outer指向了函数创建时的词法环境。而我们知道bar函数在全局运行上下文上创建的,创建时的词法环境为全局词法环境(GlobalEnvironment)。因此outer实际是指向全局词法环境。
      • 所以这里你应该清楚了,函数运行时的词法环境由两部分组成:“先天” + “后天”,先天就是函数创建时的词法环境,后天就是运行时新创建的词法环境,两个链在一块:

        我为什么一直强调"函数创建时的词法环境",因为这个很重要:就是函数运行时的词法环境和它被调用时那一刹那的词法环境无关,而只与它被创建时的词法环境相关。

        好了,bar的运行上下文创建完了,接着开始扫码函数里的代码。

  2. var声明和函数声明扫描scan:

    • 扫描到var声明:“var a = 5;”

      • 把a登记到当前的词法环境
      //注意:此次在栈顶的是bar的运行上下文
      //所以getRunningExecutionContext().LexicalEnvironment返回的是bar函数的词法环境
      var currentEnvironment = Runtime.getRunningExecutionContext().LexicalEnvironment;
      currentEnvironment.EnvironmentRecord.initialize('a',2);

      这时图上看是这样的:

      bar里面只有一个声明,接着执行语句。

  3. 执行语句

    • 执行语句:a = 5;

    • 执行函数调用foo(): 和执行bar过程类似,不再赘述,创建一个新的运行上下文,并进入栈顶

      从图中,我们看一看出,foo运行时的词法环境和foo刚刚被调用那时刻的词法环境没关系。只和它创建时的词法环境相关。

      当foo中执行语句:“console.log(a)”时候,会去当前的词法环境查找a,图中可以看出,当前词法环境是空的,因此就找当前词法环境的outer---也就是函数创建时的词法环境(保存在函数内部属性[[scope]]中),也就是全局词法环境,找到了a:2,因此打印2。

函数运行完返回的动作

函数运行完毕的返回值,分两种情况:

  • new 调用:

    • 无return语句或者有return语句但返回值不是对象:返回新创建的对象
    • 有return语句且返回值是对象:返回指定的值
  • 其他调用方式

    • 如果函数无return 语句,返回undefined

      • 有return语句,则返回return语句的值

返回后,把函数的运行上下文出栈。

最终把运行栈清空:

看图中的对象结构,已经没代码引用它们。它们孤零零的存在内存中,后续就会被js引擎的垃圾回收机制给清除。


作者:G哥讲码堂
来源:掘金

查看原文

赞 1 收藏 1 评论 0

Oliver 发布了文章 · 3月5日

JavaScript — 事件循环 eventloop

前提梳理

  1. eventloop机制是C++写的,不属于JS。JS是个单线程的语言,来了任务就执行。
  2. eventloop机制在nodejs和chrome浏览器中是不同的。下面我们先梳理nodejs,再讲chrome

Node.js的eventloop

概念图

timers  --->   poll   --->  check
  ↑      在poll阶段停留等待    |
  𠃊_________________________𠃎
  

当执行一个异步任务发生了什么(理想情况)

  • 普通异步任务

    setTimeout(fn,1000)
    1. JS把setTimeout放在timers阶段的一个数组里,并记录下1000ms
      [{event:fn,time:1000ms}]
    2. JS就不管setTimeout这个任务了,继续去执行JS同步代码
    3. 在poll阶段等待1000ms,时间到后到timers阶段执行fn
  • 普通异步任务+setImmdiate

    setTimeout(fn1,1000)
    setImmidiate(fn2)
    1. JS把setTimeout放在timers阶段的一个数组里,并记录下时长1000ms
    2. JS就不管setTimeout这个任务了,继续去执行JS同步代码
    3. 在poll阶段等待,这时候发现setImmideate任务来了,不再等待,跳到check阶段
    4. check阶段有个数组,里边存放了fn2. 带着fn2跳转到timers执行
    5. 执行后发现1000ms还是没到,于是回到poll阶段等待,到了1000ms后再经过check阶段跳转到timers,执行fn

奇怪现象

相信你注意到了上个板块中的“理想情况”四个字。
什么叫理想情况?那就是我们假定eventloop的启动要比js的启动更快。

//test.js
setTimeout(()=>{ console.log('fn1') },1000)
setImmidiate(()=>{ console.log('fn2') })

$ node test.js 
//输出:fn1 fn2
$ node test.js
//输出:fn2 fn1

你会发现,同样的代码居然产生了不一样的结论

所以说,eventloop和js代码,谁启动的更快就决定了执行的顺序!

  • 假设eventloop更快:那么timers阶段,数组里并没有存入setTimout,直接进入poll阶段等待,这时候js才执行。那eventloop先执行fn2,在下一轮执行fn1
  • 假设js执行更快:那么timers阶段,数组里就已经存在了setTimeout,那么久先执行fn2,再执行fn2

复杂题目

const fn = ()=>{
    setImmidiate(()=>{
      console.log('a')
      setTimeout(()=>{
         console.log('b')
     },0)
    })
    setTimeout(()={
     console.log('c')
     setImmidiate(()=>{
            console.log('d')
        })
    },0)
}
fn()

输出:a c b d

Chrome的eventloop

宏任务和微任务

  1. 宏任务:一会儿就做的异步任务

    • setTimeout
    • setInterval
    • script(整体代码)
    • UI渲染
  2. 微任务:马上就做的异步任务

    • Promise.then
    • Promise.catch

运行机制

执行顺序

  1. 代码从开始执行调用一个全局执行栈,script标签作为宏任务执行
  2. 执行过程中同步代码立即执行,异步代码放到任务队列中,任务队列存放有两种类型的异步任务,宏任务队列,微任务队列。
  3. 同步代码执行完毕也就意味着第一个宏任务执行完毕(script)
    (1)先查看任务队列中的微任务队列是否存在宏任务执行过程中所产生的微任务
    1-1。 有的话就将微任务队列中的所有微任务清空

    1-2。 微任务执行过程中所产生的微任务放到微任务队列中,在此次执行中一并清空

    (2)如果没有再看看宏任务队列中有没有宏任务,有的话执行,没有的话事件轮询第一波结束

实例

setTimeout(() => console.log(4)) 
new Promise(resolve => {  
   resolve() 
   console.log(1) 
}).then(() => { 
   console.log(3) 
}) 
console.log(2)

输出: 1 2 3(微任务) 4(宏任务)

进阶题目

async function fn1(){
    console.log(1)
    await fn2()
    console.log(2)
}
async function fn2(){
    console.log(3)
}
fn1()

new Promise(function(resolve){
    console.log(4)
    resolve()
}).then(()=>{
    console.log(5)}
)

输出:1 3 4 2 5

tips:

  • await 展开

    await fn2(); console.log(2)
    //可以合并为 
    fn2().then(()=>{ console.log(2) })
查看原文

赞 1 收藏 1 评论 0

Oliver 发布了文章 · 2月29日

React Hooks原理 — useState 简易版实现

引入

当你看到这样一串代码,你是怎么想的?

function  App()  {
    const  [n, setN] =  React.useState(0);
    return  (
        <div  className="App">
            <p>{n}</p>
            <button  onClick={() =>  setN(n + 1)}>+1</button>
        </div>
    );
}
ReactDOM.render(<App  />,  rootElement);

我来猜一猜:

  1. 用useState声明了一个n,一个改变n值的方法setN,让n初始值为0
  2. 调用setN之后n的值会发生改变

但过程真的是这样的吗?

console.log

  1. 在App组件内加一句console.log('App');
    你会发现App在每一次点击button,都进行了刷新
    每次setN都重新进行了render
  2. 在App组件内加一句console.log(n)
    你会发现每次点击button,打出的n都是不同的,第一次是0,第二次是1,第三次是2 ...
    useState(0)中的0只在第一次初始化生效(否则每一次执行n都是0)

雏形

function  myUseState(initialValue)  {
    var  state  =  initialValue;
    function  setState(newState)  {
        state  =  newState;
        render();
    }
    return  [state,  setState];
}
const render = () => ReactDOM.render(<App  />, rootElement);

点击后发现了什么呢? n的值并没有改变。
因为每一次myUseState都把n初始化成了0

改良

let myState
function  myUseState(initialValue)  {
    var  myState  = myState===undefined? initialValue : myState;    //控制初始化
    function  setState(newState)  {
        myState  =  newState;
        render();
    }
    return  [myState,  setState];
}
const render = () => ReactDOM.render(<App  />, rootElement);
  • 经过改造,myUseState终于正常运行了
  • 你也可以看到,setN根本没有改变n,它改变了一个myState的值,保存在其他地方
  • 在调用setN的时候,实际上是把myState的值赋给n,并且重新进行了rerender,才给你造成了setN改变了n的错觉

我用了好多个useState咋办?

方案 1 : 把myState做成对象 {n:0, m:1}

  • useState()只能传一个参数,没法区分键名n和m

方案 2 : 把myState做成数组 [0, 1]

  • 可行(就是这种思路实现的)

但是!方案 2 会区分顺序!!
所以还需要index来辅助记录顺序
这也就导致了我们在使用useState的时候必须写在顶层,不能放在判断语句中,否则就会打乱数组的顺序造成错误。

我在好多个组件都用了useState咋办?

方案1:放在全局变量里

  • 只有智障才会放在全局变量里

方案2:放在虚拟React节点中

  • 虚拟react节点的数据类型是对象,正好可以在每一个组件的节点上加上 myState 和 index 的键值对!完美解决
查看原文

赞 0 收藏 0 评论 0

Oliver 发布了文章 · 2月22日

async-await promise — 异步处理

Promise 例子

function fn() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const n = Math.random();
      n >= 0.5 ? resolve(n) : reject(n);
    }, 2000);
  });
}
 fn().then(val => { console.log(val, 'succeed'); })
    .catch(val => { console.log(val, 'failed'); });

如果then方法里又返回了promise对象,则代码风格会比较散乱,可控性差。

async-await 改造

function fn() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const n = Math.random();
      n >= 0.5 ? resolve(n) : reject(n);
    }, 2000);
  });
}
async function count() {
  try {
    const number = await fn();
    console.log('success', number);
  } catch (errorNumber) {
    console.log('fail', errorNumber);
  }
}
count();
  1. async-await 允许你用同步的代码风格编写异步代码。
  2. async-await 实际上就是 promise 的语法糖,resolve 后的参数会通过 await 赋值给 number , reject 后的参数会赋值 errorNumber。

async-await 用法解释

  1. await 只能在 async 函数内使用
  2. await 后面接一个 promise 对象(或者 return Promise() 的函数)
  3. async-await 没有处理错误的方式,所以需要用 try catch 方式进行错误处理
查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 32 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2019-07-28
个人主页被 941 人浏览