Giter Club home page Giter Club logo

step-by-step's People

Contributors

into-piece avatar

Stargazers

 avatar  avatar  avatar  avatar

Watchers

 avatar

step-by-step's Issues

JSONP原理及简单实现

同源策略

同源协议指的是页面若域名,端口,协议都相同,便具有相同的源。
目的是为了保证用户信息的安全,防止恶意的网站窃取数据。
解决这种同源策略的方法便成为跨域。

JSONP

JSONP是JSON with Padding的略称。允许在服务器端集成Script tags返回至客户端,通过javascript callback的形式实现跨域访问。

原理

利用script标签的src属性调用资源不跨域的特性,向服务端请求同时传一个callback回调方法名作为参数,服务端接受函数名生成返回json格式资源的代码。

实现

<script type="text/javascript">
    // 获取到跨域资源后的回调
    var handleFn = function(data){
        console.log(data)  // JSONP跨域成功返回的资源
    };
    var url = "resource-url?callback=handleFn";
    var script = document.createElement('script');
    script.setAttribute('src', url);
    document.getElementsByTagName('head')[0].appendChild(script); 
</script>
// 服务端
handleFn({
    "date": "2019-6-18",
    "slogan": "夕夕姐真好看",
    "content": "稳坐沙发" 
});

20200428

URLSearchParams

var paramsString = "q=URLUtils.searchParams&topic=api"
var searchParams = new URLSearchParams(paramsString);

for (let p of searchParams) {
  console.log(p);
}

searchParams.has("topic") === true; // true
searchParams.get("topic") === "api"; // true
searchParams.getAll("topic"); // ["api"]
searchParams.get("foo") === null; // true
searchParams.append("topic", "webdev");
searchParams.toString(); // "q=URLUtils.searchParams&topic=api&topic=webdev"
searchParams.set("topic", "More webdev");
searchParams.toString(); // "q=URLUtils.searchParams&topic=More+webdev"
searchParams.delete("topic");
searchParams.toString(); // "q=URLUtils.searchParams"

手动实现

class URLSearchParams {
  constructor(obj) {
    this.params = new Map();
    if (Object.prototype.toString.call(obj) === "[object String]") {
      obj.split("&").forEach((item) => {
        const [key, value] = item.split("=");
        this.params.set(key, value);
        console.log(this.params);
      });
    }
    if (obj instanceof Object) {
      Object.keys(obj).forEach((item) => {
        this.params.set(item, obj[item]);
      });
    }
  }

  get(k) {
    return this.params.get(k);
  }

  set(k, v) {
    return this.params.set(k, v);
  }

  has(k) {
    return this.params.has(k);
  }

  append() {}

  toString() {
    // for (let [key, value] of this.params.entries()) {

    // }
    // [...map.entries()]
    // this.params.forEach(function(value, key, map) {
    //   str+= `${key}=${value]}`
    // });

    const arr = [...this.params];
    const str = arr.reduce((res, [key, value], index) => {
      res += `${key}=${value}${index === arr.length - 1 ? "" : "&"}`;
      return res;
    }, "");
    return str;
  }
  

  // 可遍历
  *[Symbol.iterator]() {
    yield* this.params;
  }
}

PWA

What are Progressive Web Apps?

  • Web App Manifest
  • Service Worker
  • Cache API 缓存
  • Push&Notification 推送与通知
  • Background Sync 后台同步
  • 响应式设计

node中的vm

nodejs有一个叫vm的模块,这是用来运行代码的模块。怎么说呢,我们知道浏览器只要你将代码放在script标签中,或script src远程引用它,代码就会执行,这些代码会执行一些我们没有定义但会预先传入的对象或方法,比如window, document, location, fetch等等。我们知道其实document, location, fetch这些东西都在window上,window是一个上下文。 在iframe中也有自己的window,各自独立。而在nodejs,怎么形成这个"window"呢,这就需要vm来提供。

我们看到nodejs通过require也能运行其他模块,这是因为它们用到vm中的方法。

Safeify node vm沙盒实现

const vm = require('vm');
const script = new vm.Script('m + n');
const sandbox = { m: 1, n: 2 };
const context = new vm.createContext(sandbox);
script.runInContext(context);

【nodejs】eventloop + 线程池

线程池

频繁创建和销毁线程对性能的影响显而易见,同时这样的设计并不能撑其瞬时峰值流量。
线程池就是进行线程的生命周期管控,达成=》线程复用

libuv threadPool 源码地址=》 源码地址

线程池利用死循环让线程无法结束,在等待任务期间处于阻塞状态,利用阻塞唤醒来让线程接收任务(本质上阻塞唤醒基于信号量),从而达到线程复用,结束当前任务后进入下一次循环,周而复始。

eventloop + 线程池 = 异步非阻塞

可爱的你发起了一个 IO 调用,从 《大前端进阶 Node.js》系列 异步非阻塞中讲过,一个 IO 调用要么是阻塞调用,要么是先非阻塞的发起 IO,再在需要看结果的时候阻塞的去获取,显然这两种模式都不是我们想要的。

我们要的是异步非阻塞,所以这个 IO 调用一定不是在主线程中执行,这个时候我们就能联想到上面的线程池。

主线程不能被阻塞,但线程池里面的线程可以,主线程只需要把 IO 调用交给线程池来执行,自己就可以愉快的玩耍,以此达到了我们的第一个目标:非阻塞。

在线程池 IO 处理结束后,会主动的把结束的请求放入 eventloop 的观察者(watcher)中,也就是我们的 queue 中,eventloop 处于不断循环的状态,当下一次循环 check 到 queue 里有请求的时候,就会取出来然后执行回调,这样我们想要的异步就达到了。

最终通过线程池和 eventloop 结合,呈现出的效果就是,当你发起一次 IO 调用,你无需阻塞的等待 IO 结束,也无需在想利用 IO 结果的时候不断的轮询,整个 IO 过程对主线程而言非阻塞,并且自动结束时执行回调,达到我们想要的异步非阻塞。

核心总结:Node 利用线程池来执行 IO 调用,避免阻塞主线程,执行结束后把结果和请求放入一个队列,利用事件循环来取出队列的请求,最后执行回调,达到了异步非阻塞的效果。

系统架构

  • Nginx 负载均衡
  • Node service层
  • Redis 在内存中进行数据结构存储 (db缓存
  • Kafka 做消息队列
  • MySQL 数据库层

前端层面

在抢单系统中,前端页面如若面临成千上万个需求,需要做的是限制用户频繁出发请求事件。对触发事件进行节流固然可以,但直接拿接口来发送请求,这个时候我们要防止恶意触发,需要做好防csrf攻击(跨站点请求伪造)。

  1. Referer验证
  2. 在请求地址中添加 token 并验证
  3. set cookie的samesite。
Set-Cookie: key=value; SameSite=Strict

Nginx层

当我们做一个抢单系统的时候,面对成千上万个请求首先考虑的,便是怎么分担服务器的压力,这时候负载均衡

referer-policy

Referer这个http header的参数应用得当的话,是可以提高安全性的,比如,可以这个参数其实就告诉了链接的请求来源于哪个网站,所以可以根据这个特性,限制一些接口只能本网站的才能调,外部网站不能调

  • "no-referrer"
  • "origin"
  • "unsafe-url"
  • "strict-origin"
  • "strict-origin-when-cross-origin"
  • "same-origin"
  • "no-referrer-when-downgrade"

请求头中

Referrer-Policy: origin

meta标签

<meta name="referrer" content="origin">

a标签

<a href="..." referrerpolicy="origin" target="_blank">xxx</a>

20200506面试总结

call apply 作用和区别

  • 作用: 让方法可以调用对象this属性上的方法或属性
  • 区别:接受参数不一样,call接受第一个参数为对象,后面为执行函数的参数,但apply为数组形式。

说说快速排序

通过把数组中数字与最后一个数字对比分为三部分,左右部分再递归进行大小对比进行排序。

实现随机颜色值

如何提升 webpack 的打包速度

json.stringify 需要注意什么

undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。函数、undefined 被单独转换时,会返回 undefined,如JSON.stringify(function(){}) or JSON.stringify(undefined)。

tcp udp 的区别

  • TCP 是面向连接的,UDP 是面向无连接的
  • TCP 是面向字节流的,UDP 是基于数据报的
  • TCP 保证数据正确性,UDP 可能丢包
  • UDP程序结构较简单,只有端口号

应用场景:

udp:需要处理速度快,可以容忍丢包,如直播,实时游戏之类的

TCP 为什么是可靠连接

  • 通过 TCP 连接传输的数据无差错,不丢失,不重复,且按顺序到达。
  • TCP 报文头里面的序号能使 TCP 的数据按序到达
  • 报文头里面的确认序号能保证不丢包,累计确认及超时重传机制
  • TCP 拥有流量控制及拥塞控制的机制

数组去重

  • Set对象的特性=》new set([1,2,2,3])
  • 新建一个空的结果数组,for 循环原数组,判断结果数组是否存在当前元素,如果有相同的值则跳过,不相同则push进数组。
  • reduce+includes
function unique(arr){
    return arr.reduce((prev,cur) => prev.includes(cur) ? prev : [...prev,cur],[]);
}

object 和 map 的区别

object的key只能是字符串,map的key可以是任何数据类型

  • 键:Object遵循普通的字典规则,键必须是单一类型,并且只能是整数、字符串或是Symbol类型。但在Map中,key可以为任意数据类型(Object, Array等)。(你可以尝试将一个对象设置为一个Object的key,看看最终的数据结构)
  • 元素顺序:Map会保留所有元素的顺序,而Object并不会保证属性的顺序。(如有疑问可参考:链接)
  • 继承:Map是Object的实例对象,而Object显然不可能是Map的实例对象。

Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。

你可以通过size属性很容易地得到一个Map的键值对个数,而对象的键值对个数只能手动确认。很多方法都非常方便,has可以得知是否存在某个属性。

说说websocket

HTTP是不支持持久连接的,keep-alive只是把多个连接合并为一个。websocket是h5推出的一个是一个持久化协议,只要建立一次连接,就可以持续接收服务端发送的数据。

解决了HTTP轮询(得不到就一直请求)和long poll(发送堵塞直到得到数据返回)同步有延迟,消耗资源(HTTP是非状态性的,每次建立连接都是需要鉴别身份)

基本数据类型有哪些

string number boolean null undefined object symbol bigint

new 一个对象的过程

new 运算接受一个构造器和一组调用参数,实际上做了几件事:

  • 创建一个继承构造器的原型对象的新对象作为实例;
  • 将 this 和调用参数传给构造器,执行;
  • 判断执行返回结果是否为对象,若是则返回,否则返回创建的实例对象

promise 原理 ,then 实现

在promise中用回调数组保存then传入的回调,等待异步执行完毕后,resolve被调用执行then保存的对应的fullfilled回调或reject回调。

then = (successfunc: Func, failfunc?: Func) => {
    return new Promise((resolve, reject) => {
      try {
        if (this.status === PENDING) {
          successfunc &&
            this.successCallbackList.push((v: any) => {
              setTimeout(() => successfunc(v));
            });
          failfunc &&
            this.failCallbackList.push((v: any) => {
              setTimeout(() => failfunc(v));
            });
        }

        // 第二次第三次then的时候 状态已经改完成功或者失败
        if (this.status === PENDING) {
          const res = successfunc(this.value);
          resolve(res);
        }

        if (this.status === REJECTED) {
          const res = failfunc && failfunc(this.value);
          resolve(res);
        }
      } catch (e) {
        reject(e);
      }
    });
  };

eventloop 机制介绍

async wait 机制

bfc 块级格式上下文

缓存和强缓存

  • 强缓存: expires(HTTP1.0)(时间戳) 和cash-control(http1.1)(优先级更高)
  • 协商缓存:last-modified(资源的最新更新时间)和e-tag(资源唯一标示)

洗牌算法

https 原理

SSL和CA数字证书结合

https分为两个阶段,数字证书验证阶段和数据传输阶段。

  • 首先服务器用RSA生成公钥和私钥
  • 在数字证书验证阶段服务器把公钥放在证书里发送给客户端。
  • 客户端向一个权威的服务器检查证书的合法性,如果证书合法就生成随机数并通过公钥进行加密。发送到服务端
  • 服务端通过私钥进行解密,以此解密后的随机数作为数据传输阶段中对称加密的密钥,对后面传输的数据进行加密。

react 性能优化

  • 函数组件的memo
  • 类组件的purecomponent和scp + immer
  • hooks的useCallback和useMemo
  • lazy和suspense=》code split
  • 减少副作用 + render回调使函数用引用
  • 使用React.Fragment避免添加额外的DOM
  • ssr: Next.js

整体来说 react 服务端渲染原理不复杂,其中最核心的内容就是同构。
node server 接收客户端请求,得到当前的req url path,然后在已有的路由表内查找到对应的组件,拿到需要请求的数据,将数据作为 props
、context或者store 形式传入组件,然后基于 react 内置的服务端渲染api renderToString() or renderToNodeStream() 把组件渲染为 html字符串或者 stream 流, 在把最终的 html 进行输出前需要将数据注入到浏览器端(注水),server 输出(response)后浏览器端可以得到数据(脱水),浏览器开始进行渲染和节点对比,然后执行组件的componentDidMount 完成组件内事件绑定和一些交互,浏览器重用了服务端输出的 html 节点,整个流程结束。

express 和 koa 的区别,洋葱模型
Express 和 Koa 最明显的差别就是 Handler 的处理方法,一个是普通的回调函数,一个是利用生成器函数(Generator Function)来作为响应器。往里头儿说就是 Express 是在同一线程上完成当前进程的所有 HTTP 请求,而 Koa 利用 co 作为底层运行框架,利用 Generator 的特性,实现“协程响应”,异步处理能力大大增强。

koa 没有内置router,view,给了社区很大的自由

如何实现一个画板,如何让画笔更流畅
如何实现扑克牌的反转效果
使用ajax下载文件
如何实现富文本编辑器
node 的模块能在浏览器中执行吗?

react hook 的理解和应用
node 多进程的通信方式
taro的原理
node 服务如何处理错误和异常
http1 和 http2 的区别
两数之和(数组内找出2个数的和值)

作用域,闭包
let var 区别,let 为什么能实现块儿作用域
js 处理代码的过程
react 生命周期执行过程 ,包括子组件
react setState 过
fiber 机制
diff 算法
http 请求过程
缓存机制的处理过程
vue 和 react 区别,
koa 中间件机制,解决了什么问题

手写call,apply,bind,new

function call (){
  let [ctx,...args] =  Array.from(arguments);
  ctx.fun = this || window;
  const res = args.length>0 ? ctx.fun(...args) : ctx.fun();
  delect ctx.fun;
  return res;
}

function apply () {
  let [ctx,args] =  Array.from(arguments);
  ctx.fn = this || window;
  const res = args.length>0?ctx.fn(args):ctx.fn();
  delect ctx.fn;
  return res;
}

function myNew(){
  let target = Object.create(null);
  let [constructor,...args] =  Array.from(arguments);
  target._proto_ = constructor.prototype
  const res = constructor.apply(target,args)
  if(res&&(typeof res == 'object'|| typeof res == 'function')){
    return res
  }
  return target
}

关于ref

使用 React Hooks 声明 setInterval

function Counter() {
  let [count, setCount] = useState(0);

  useEffect(() => {
    let id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  },[]);

  return <h1>{count}</h1>;
}

但是,这代码有一个奇怪的行为。
默认情况下,React 会在每次渲染后重执行 effects,这是有目的的,这有助于避免 React class 组件的某种 bugs。
这通常是好的,因为需要许多订阅 API 可以随时顺手移除老的监听者和加个新的。但是,setInterval 和它们不一样。当我们执行 clearInterval 和 setInterval 时,它们会进入时间队列里,如果我们频繁重渲染和重执行 effects,interval 有可能没有机会被执行!
获取不到新的值,useEffect没有依赖count

function Counter() {
  const [count, setCount] = useState(0);
  const func = useRef();

  func.current = () => {
    console.log(count);
    setCount(count + 1);
  };

  useEffect(() => {
    let id = setInterval(() => {
      func.current();
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <h1>{count}</h1>;
}

或者

setCount((pre)=>pre + 1);

Promise全解析

promise

const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
class MyPromise<P> {
  fn: any;
  status: string = PENDING;
  resolveCallback: Array<any> = [];
  rejectCallback: Array<any> = [];
  value: any;
  reason: any;

  constructor(fn: (resolve: (value: any) => any, reject: (value: any) => any) => void) {
    this.fn = fn;
    fn(this.resolve, this.reject);
  }

  resolve = (value: any) => {
    if (this.status === PENDING) {
      this.status = FULFILLED;
      this.value = value;
      this.resolveCallback.forEach(fn => {
        fn(value);
      });
    }
  };

  reject = (reason: any) => {
    if (this.status === PENDING) {
      this.status = REJECTED;
      this.reason = reason;
      this.rejectCallback.forEach(fn => fn());
    }
  };

  /**
   * then 判断当前状态
   * 如果调用 then 时,promise已经成功,则执行 onFulfilled,并将promise的值作为参数传递进去。
   * 如果promise已经失败,那么执行 onRejected, 并将 promise 失败的原因作为参数传递进去。
   * 如果promise的状态是pending,需要将onFulfilled和onRejected函数存放起来,等待状态确定后,再依次将对应的函数执行(发布订阅)
   * 这里需要用setTimeOut模拟异步,因为promise是then才是异步
   */
  then = (onFulfilled: any, onRejected?: any) => {
    console.log(onFulfilled, this.status, '====');

    return new Promise((resolve, reject) => {
      if (this.status === FULFILLED) {
        setTimeout(() => {
          try {
            setTimeout(() => {
              onFulfilled(this.value);
            });
          } catch (e) {
            reject(e);
          }
        });
      }

      if (this.status === REJECTED) {
        try {
          setTimeout(() => {
            onRejected(this.reason);
          });
        } catch (e) {
          reject(e);
        }
      }

      if (this.status === PENDING) {
        this.resolveCallback.push(() => {
          setTimeout(() => {
            onFulfilled(this.value);
          });
        });
        this.rejectCallback.push(() => {
          setTimeout(() => {
            onRejected(this.reason);
          });
        });
      }
    });
  };
}

const a = new MyPromise((resolve, reject) => {
  // setTimeout(() => {
  // resolve(1);
  // reject(1);
  // }, 1000);

  // reject(1);

  resolve(1);
}).then(
  (v: any) => {
    console.log(v);
  },
  (error: any) => {
    console.log(error);
  }
);

promise all

Promise.myAll = function(promises) {
  let results = [];
  let count = 0;
  let len = promises.length;
  return new Promise(function(resolve, reject) {
    if (!(promises instanceof Array))
      return reject("arguments must be an array");
    for (let val of promises) {
      Promise.resolve(val).then(
        function(res) {
          count++;
          results.push(res);
          if (count === len) {
            return resolve(results);
          }
        },
        function(err) {
          return reject(err);
        }
      );
    }
  });
};

判断字符串中出现次数最的字符,并统计字符

function countString(string) {
  let arr = string.split("");
  let obj = arr.reduce((res, cur, index) => {
    res = {
      ...res,
      [cur]: res[cur] ? res[cur] + 1 : 1
    };
    if (index === arr.length - 1) {
      let biggst = {};
      for (let i in res) {
        if (!biggst.num || biggst.num < res[i]) {
          biggst = {
            index: i,
            num: res[i]
          };
        }
      }
      return biggst;
    } else {
      return res;
    }
  }, {});
  return obj;
}
let result = countString("12345678912111111");
console.log(result, "=================");

浏览器渲染

浏览器渲染过程

image

GPU加速

在合成的情况下,会直接跳过布局和绘制流程,直接进入非主线程处理的部分,即直接交给合成线程处理。交给它处理有两大好处:

  1. 能够充分发挥GPU的优势。合成线程生成位图的过程中会调用线程池,并在其中使用GPU进行加速生成,而GPU 是擅长处理位图数据的。

  2. 没有占用主线程的资源,即使主线程卡住了,效果依然能够流畅地展示。

如何开启?

  1. will-change:添加 will-change: tranform ,让渲染引擎为其单独实现一个图层,当这些变换发生时,仅仅只是利用合成线程去处理这些变换,而不牵扯到主线程,大大提高渲染效率。当然这个变化不限于tranform, 任何可以实现合成效果的 CSS 属性都能用will-change来声明。

  2. 开启transform3d(0,0,0)/transform:translateZ(0),开启GPU硬件加速模式,从而让浏览器在渲染动画时从CPU转向GPU

斗鱼面试题

function lottery(whiteList, participant){}

whiteList:类型字符串数组,意义是表示从其他系统中计算出来的活跃用户,如果这批用户参与抽奖,则必定让他中奖。长度不超过1万

participant:类型字符串数组,意义是表示此次活动中真正参与抽奖的用户,长度约是10万。

函数希望从participant返回 2 万个用户,表示中奖用户,优先选取whiteList上的用户,若不在whiteList上,对participant 剩余的随机选取即可。

react灵魂拷问

为什么要引入 React?

讲这个我们就要先从源头讲起,我们先来思考俺们在react组件中写的div标签,是会原封直接render输出到真实dom上吗?好像也啥问题,难道还能变成其他标签不成?

这个时候又有个疑问,input标签上其实是没有onChange事件,如果直接输出事件为什么会生效呢?

嘻嘻倒是不会变成其他标签,但是我们写的标签其实是被转化成一个个reactElement对象元素,上面配置的属性会被存储props中进行处理,onchange是react提供给我们的合成事件。Actually我们写的jsx中写的

const title = <h1 className="title">intopiece</h1>

会被babel转化为

const title = React.createElement(
    'h1',
    { className: 'title' },
    'intopiece'
);

因为从本质上讲,JSX 只是为 React.createElement(component, props, ...children) 函数提供的语法糖。听说babel 7.9支持自动导入 jsx 了,以后就不用引入了。

为什么 constructor 里要调用 super 和传递 props

这个其实是es6 Class的语法,熟读阮一峰大神的ECMAScript 6 入门应该了然于心,在Class的继承章节中:

子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。

再来是我们的component不就是继承React.Component,继承了父类提供给我们的生命周期和render等方法。

ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。

如果子类没有定义constructor方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有constructor方法。

class ColorPoint extends Point {
}
// 等同于
class ColorPoint extends Point {
  constructor(...args) {
    super(...args);
  }
}

所以正常情况下我们可以省略constructor方法的书写。

但有些扑朔迷离的是,即便你调用 super() 的时候没有传入 props,你依然能够在 render 函数或其他方法中访问到 this.props。(如果你质疑这个机制,尝试一下即可)

那么这是怎么做到的呢?事实证明,React 在调用构造函数后也立即将 props 赋值到了实例上:

  // React 内部
  const instance = new YourComponent(props);
  instance.props = props;

因此即便你忘记了将 props 传给 super(),React 也仍然会在之后将它定义到实例上。

setState什么时候异步什么时候同步?

官网做了一番解释:链接

将 setState() 视为请求而不是立即更新组件的命令。为了更好的感知性能,React 会延迟调用它,然后通过一次传递更新多个组件。React 并不会保证 state 的变更会立即生效。

在我的走进react fiber中详细解释了,其实一次setState就是一次生成一次update,连续多次setState会被放到UpdateQueue中,等待调度一起执行后render达成batchUpdate。

setState在原生事件,setTimeout,setInterval,Promise等异步操作中,state会同步更新。

diff完成后如何合并两个虚拟dom树的差异之处?

答:fiber树有两颗,当前的current和新生成的workinprogress,。当React经过当前树时,对于每一个先存在的fiber节点,它都会创建一个替代(alternate)节点,这些节点组成了workInProgress树。这个节点是使用render方法返回的React元素的数据创建的。一旦更新处理完以及所有相关工作完成,React就有一颗替代树来准备刷新屏幕。一旦这颗workInProgress树渲染(render)在屏幕上,它便成了当前树。

function createWorkInProgress(current, ...) {
  let workInProgress = current.alternate;
  if (workInProgress === null) {
    workInProgress = createFiber(...);
  }
  ...
  workInProgress.alternate = current;
  current.alternate = workInProgress;
  ...
  return workInProgress;
}

入门:为啥需要bind(this)

答:function的this是在运行时的作用域决定的,默认的this会自动指向本身的实例,但无论是在在onclick或者函数中调用自身方法,执行时已经无法保证是当前作用域(即this的指向是指向本身实例),所以无法拿到绑定在实例prototype上的对应方法。

这个可以了解一下react的合成事件系统,input本身是没有onchange,onchang会被编译为reactElement的props中的一个属性,最后所有的回调事件都会被绑定到document节点上达成事件委托以提高性能,这个时候其实已经丢失了原有的this。

bind(this):在constructor中进行绑定this,能让该方法的this指向在实例化后始终指向自身实例。

箭头函数:es6类现在已经不用babel就可以写箭头函数了,箭头函数与不同函数不同,实例化时是在constructor作为类的属性被赋值,箭头函数的特性是在定义的时候确定this指向。

为什么要有自己一套事件机制

当我们在组件上设置事件处理器时,React并不会在该DOM元素上直接绑定事件处理器. React内部自定义了一套事件系统,在这个系统上统一进行事件订阅和分发.

具体来讲,React利用事件委托机制在Document上统一监听DOM事件,再根据触发的target将事件分发到具体的组件实例。另外上面e是一个合成事件对象(SyntheticEvent), 而不是原始的DOM事件对象。

首先我们都知道react号称write once, run everything,那么像input中输入事件在不同的环境如浏览器,原生等表现和控制都是不一样的,而react可以提供一个统一的onChange合成事件,通过对不同环境作以区分来控制表现的一致性。

  • 抹平浏览器之间的兼容性差异
  • 抽象跨平台事件机制
  • 自定义一些高等级的事件

其次,我们react fiber重中之重是对不同的任务进行优先级调度,而对不同的事件进行优先级的定义和判断是一个前提,自己的事件机制也可以帮助我们react调度系统更好地进行不同优先级任务区分是悬停等待还是执行,以达成更优秀的用户体验。

虚拟dom和手动操作dom性能到底谁比较好?

https://www.zhihu.com/question/31809713/answer/53544875

以往框架一旦发现数据发生了变动就对整个页面进行更新。这样的做法效率低下,因为数据的变动而导致的页面变动很可能只是局部的,直接对整个页面进行更新造成了不必要的性能消耗。

相比直接手动操作虚拟dom,虚拟dom其实多了一个diff的过程,

在react 16后react分为reconciler协调阶段和renderer阶段两个阶段,协调阶段便负责构建虚拟dom树,在发生数据更新时两个虚拟DOM做合并操作(diff),得到更新的操作集(patch),基于新旧两棵树之间的差别来判断如何有效率的更新 UI 以保证当前 UI 与最新的树保持同步,计算树哪些部分需要更新。

我个人觉得虚拟dom最大的优势不是性能,而是大大的解放了我们前端的生产力,通过数据驱动告诉react这个状态如何控制对应的视图,只要更改对应状态便能实现视图的更新,我们完全脱离了以往需要手动调取浏览器提供ap进行dom节点更改的jq时代,并且本身是一个脱离开发环境桎梏的高级抽象,可以根据不同的宿主生成不同的宿主实例,根据对应的渲染器如React DOM、React Native达成跨平台的开发效果。

hooks写redux实战

首先我们要了解redux的优势

背景

随着React Fiber架构的革新,意在为优化用户体验实现任务优先级调度,实现fiber节点存储组件数据的数据结构的同时,顺带衍生了新的开发模式hooks,这无疑为react走向函数式编程的征程推进了一大步。

首先我们来看官方对hooks出现的动机:

  • 难以复用类组件之间的逻辑
  • 生命周期中经常包含一些莫名其妙的不相关逻辑
  • 类组件难以被机器和人理解

但步子迈太大容易扯到内啥,

需要了解以下几个hook,

useReducer

const [state, dispatch] = useReducer(reducer, initialArg, init);

看到这个如此熟悉的名字,大概能猜出这个hooks的使用机制,

useContext与createContext


问题

  • redux和现在的状态管理数据流向

  • Context.Provider的value更新的时候,会默认通知所有使用了useContext该Context订阅的子组件重新渲染。这个在项目复杂的时候,性能上会是灾难

qq

1、自我介绍。
2、前端假设白屏,不看报错,怎么判断错误。
3、http请求过程。
4、http的chunk的作用。
5、接触前端的新的技术吗? flutter原理是什么?一套UI组件是怎么绘制的?
6、http怎么保证数据稳定传输,具体怎么传输的。如果数据包错误怎么办?
7、操作系统的虚拟内存有什么作用?
8、多线程会增加哪些消耗?
9、js为什么是单线程?
10、8X8棋盘,写个算法判断是否重装。

盒模型和bfc

盒模型是通过文档树中的元素和视觉可视化模型生成的矩形盒子

基础盒模型

浏览器在渲染render tree得时候,渲染引擎会根据基础盒模型将元素划分为一个个矩形盒子,这些盒子的外观,属性由css来决定。

视觉格式化模型

CSS 的视觉格式化模型(visual formatting model) 是根据 基础盒模型(CSS basic box model) 将 文档(doucment) 中的元素转换一个个盒子的实际算法。
官方说法就是: 它规定了用户端在媒介中如何处理文档树( document tree )。

CDN有哪些优化加载静态资源速度的机制?

访问网站的基本过程:

  • 用户在自己的浏览器中输入要访问的网站域名。
  • 浏览器向 本地DNS服务器 请求对该域名的解析。
  • 本地DNS服务器中如果缓存有这个域名的解析结果,则直接响应用户的解析请求。
  • 本地DNS服务器中如果没有关于这个域名的解析结果的缓存,则以递归方式向整个DNS系统请求解析,获得应答后将结果反馈给浏览器。
  • 浏览器得到域名解析结果,就是该域名相应的服务设备的 IP地址 。
  • 浏览器向服务器请求内容。
  • 服务器将用户请求内容传送给浏览器。

CDN 用户访问调度流程

  1. 当用户点击网站页面上的内容URL,经过本地DNS系统解析,DNS 系统会最终将域名的解析权交给 CNAME 指向的 CDN 专用 DNS 服务器。
  2. CDN 的 DNS 服务器将 CDN 的全局负载均衡设备 IP 地址返回用户。
  3. 用户向 CDN 的全局负载均衡设备发起内容 URL 访问请求。
  4. CDN 全局负载均衡设备根据用户 IP 地址,以及用户请求的内容URL,选择一台用户所属区域的区域负载均衡设备,告诉用户向这台设备发起请求。
  5. 基于以下这些条件的综合分析之后,区域负载均衡设备会向全局负载均衡设备返回一台缓存服务器的IP地址:
  • 根据用户 IP 地址,判断哪一台服务器距用户最近;
  • 根据用户所请求的 URL 中携带的内容名称,判断哪一台服务器上有用户所需内容;
  • 查询各个服务器当前的负载情况,判断哪一台服务器尚有服务能力。
  1. 全局负载均衡设备把服务器的 IP 地址返回给用户。
  2. 用户向缓存服务器发起请求,缓存服务器响应用户请求,将用户所需内容传送到用户终端。如果这台缓存服务器上并没有用户想要的内容,而区域均衡设备依然将它分配给了用户,那么这台服务器就要向它的上一级缓存服务器请求内容,直至追溯到网站的源服务器将内容拉到本地。

DNS 服务器根据用户 IP 地址,将域名解析成相应节点的缓存服务器IP地址,实现用户就近访问。使用 CDN 服务的网站,只需将其域名解析权交给 CDN 的全局负载均衡(GSLB)设备,将需要分发的内容注入 CDN,就可以实现内容加速了。

CDN 的核心功能,一个是缓存,一个是回源。

缓存就是说我们把资源复制一份到 CDN 服务器上,回源就是说 CDN 发现自己没有这个资源(一般是缓存的数据过期了),重新向向web 服务器(或者它的上层服务器)重新取这个资源。

CDN中一方面有很多的缓存策略,从而可以提高加载速度。 另一方面CDN有路由算法,可以选择用户访问速度最快的节点进行接入,从而加快RTT(Roud Trip Time)

CDN会挑选最优设备提供服务。可能是Cache最接近用户,或者条件最好的路径。这个过程就叫均衡负载。

typescript专题

typescript的好处

  1. 代码可读性,对组件函数的输入输出进行类型定义,方便

typescript 的 type 和 interface 的区别

  • type 可以声明基本类型、联合类型和元组,interface 不行
  • 继承 type 和 interface 不是互斥的,可以相互继承
  • 声明函数方式不一样
  • 实现:type 也可以 implements,但是不可以通过联合类型去实现类。
    你能定义多次相同的 interface,这些定义将要合并为一个。对于类型别名就不成立,因为类型别名是独一无二的实体。
  • interface可以声明合并(多次声明, type不行

typescript原理

深入理解 TypeScript

以下演示简单说明 TypeScript 编译器如何将上述几个关键部分组合在一起:

SourceCode(源码) ~~ 扫描器 ~~> Token 流
Token 流 ~~ 解析器 ~~> AST(抽象语法树)
AST ~~ 绑定器 ~~> Symbols(符号)
符号(Symbol)是 TypeScript 语义系统的主要构造块。如上所示,符号是绑定的结果。符号将 AST 中的声明节点与相同实体的其他声明相连。

符号和 AST 是检查器用来验证源代码语义的

AST + 符号 ~~ 检查器 ~~> 类型验证
最后,需要输出 JavaScript 时:

AST + 检查器 ~~ 发射器 ~~> JavaScript 代码

20200513

koa的洋葱模型实现

koa被认为是第二代node web framework,它最大的特点就是独特的中间件流程控制,是一个典型的洋葱模型。koa和koa2中间件的思路是一样的,但是实现方式有所区别,koa2在node7.6之后更是可以直接用async/await来替代generator使用中间件,本文以最后一种情况举例。

const Koa = require('koa');

const app = new Koa();
const PORT = 3000;

// #1
app.use(async (ctx, next)=>{
    console.log(1)
    await next();
    console.log(1)
});
// #2
app.use(async (ctx, next) => {
    console.log(2)
    await next();
    console.log(2)
})

app.use(async (ctx, next) => {
    console.log(3)
})

app.listen(PORT);
console.log(`http://localhost:${PORT}`);

当程序运行到await next()的时候就会暂停当前程序,进入下一个中间件,处理完之后才会仔回过头来继续处理。也就是说,当一个请求进入,#1会被第一个和最后一个经过,#2则是被第二和倒数第二个经过,依次类推。

实现

koa的实现有几个最重要的点

  1. context的保存和传递
  2. 中间件的管理和next的实现

当我们app.use的时候,只是把方法存在了一个数组里。

use(fn) {
    this.middleware.push(fn);
    return this;
}

服务端是如何做路由分发

  • 浏览器: hash和history api

操作系统 - 磁盘寻道调度算法

FIFO

先入先出队列(First Input First Output,FIFO)这是一种传统的按序执行方法,先进入的指令先完成并引退,跟着才执行第二条指令。

先来先服务算法(FCFS)

这是一种比较简单的磁盘调度算法。它根据进程请求访问磁盘的先后次序进行调度。此算法的优点是公平、简单,且每个进程的请求都能依次得到处理,不会出现某一进程的请求长期得不到满足的情况。此算法由于未对寻道进行优化,在对磁盘的访问请求比较多的情况下,此算法将降低设备服务的吞吐量,致使平均寻道时间可能较长,但各进程得到服务的响应时间的变化幅度较小。

最短寻道时间优先算法(SSTF),

该算法选择这样的进程,其要求访问的磁道与当前磁头所在的磁道距离最近,以使每次的寻道时间最短,该算法可以得到比较好的吞吐量,但却不能保证平均寻道时间最短。其缺点是对用户的服务请求的响应机会不是均等的,因而导致响应时间的变化幅度很大。在服务请求很多的情况下,对内外边缘磁道的请求将会无限期的被延迟,有些请求的响应时间将不可预期。

扫描算法(SCAN)(电梯调度算法)

循环扫描算法(CSCAN)

常见进程间通信(IPC)方式

  • 管道pipe
  • 有名管道FIFO
  • 消息队列MessageQueue
  • 共享存储
  • 信号量Semaphore
  • 信号Signal
  • 套接字Socket

从输入 URL 到页面渲染经历了什么

5个主要步骤

1 DNS 查询
2 TCP 连接
3 HTTP 请求即响应
4 服务器响应
5 客户端渲染

浏览器渲染分五个步骤

  1. 处理 HTML 标记并构建 DOM 树。

  2. 处理 CSS 标记并构建 CSSOM 树。

  3. 将 DOM 与 CSSOM 合并成一个渲染树。

  4. 根据渲染树来布局,以计算每个节点的几何信息。

  5. 将各个节点绘制到屏幕上。

  6. cdn解析:浏览器缓存=》本机缓存

  7. 建立tcp连接: 检查是否强缓存/协商缓存

  8. 请求资源html的解析(词法分析和语法分析

  9. css和js文件的请求和解析

  10. 构建的DOM树和cssom树结合生成render tree

构建对象模型

  1. 转换: 浏览器从磁盘或网络读取 HTML 的原始字节,并根据文件的指定编码(例如 UTF-8)将它们转换成各个字符。
  2. 令牌化: 浏览器将字符串转换成 W3C HTML5 标准规定的各种令牌,例如,“”、“”,以及其他尖括号内的字符串。每个令牌都具有特殊含义和一组规则。
  3. 词法分析: 发出的令牌转换成定义其属性和规则的“对象”。
  4. DOM 构建: 最后,由于 HTML 标记定义不同标记之间的关系(一些标记包含在其他标记内),创建的对象链接在一个树数据结构内,此结构也会捕获原始标记中定义的父项-子项关系:HTML 对象是 body 对象的父项,body 是 paragraph 对象的父项,依此类推。
    js执行,会堵塞GUI渲染线程,

渲染层合并

setTimeout setImmediate process.nextTick的区别

链接
Node.js的event loop及timer/setImmediate/nextTick

/process.nextTick

process.nextTick()方法的操作相对较为轻量,每次调用Process.nextTick()方法,只会将回调函数放入队列中,在下一轮Tick时取出执行。定时器采用红黑树的操作时间复杂度为o(lg(n)),而nextTick()的时间复杂度为o(1)。相较之下,process.nextTick()更高效。

setImmediate()方法和process.nextTick()方法十分类似,都是将回调函数延迟在下一次立即执行。

区别
1、process.nextTick中回调函数的优先级高于setImmediate
原因在于事件循环对观察者的检查是有先后顺序的,process.nextTick属于idle观察者,setImmediate属于check观察者。在每一轮循环检查中,idle观察者先于I/O观察者,I/O观察者先于check观察者。

多个process.nextTick语句总是一次执行完,多个setImmediate则需要多次才能执行完。

2、在实现上,process.nextTick的回调函数保存在一个数组中,setImmediate则保存在一个链表中

3、setImmediate可以使用clearImmediate清除,process.nextTick不能被清除。

观察者优先级

在每次轮训检查中,各观察者的优先级分别是:

idle观察者 > I/O观察者 > check观察者。

idle观察者:process.nextTick

I/O观察者:一般性的I/O回调,如网络,文件,数据库I/O等

check观察者:setImmediate,setTimeout

setTimeoout和setImmiediate谁快?

在一个异步流程里,setImmediate会比定时器先执行

同样在最外层的,需要看情况。

node.js里面setTimeout(fn, 0)会被强制改为setTimeout(fn, 1)

我们发现关键就在这个1毫秒,如果同步代码执行时间较长,进入Event Loop的时候1毫秒已经过了,setTimeout执行,如果1毫秒还没到,就先执行了setImmediate。每次我们运行脚本时,机器状态可能不一样,导致运行时有1毫秒的差距,一会儿setTimeout先执行,一会儿setImmediate先执行。但是这种情况只会发生在还没进入timers阶段的时候。

http2对比websocket

  • HTTP/2 Server Push 不能被代码使用,所以还得配合SSE(Server sent event),无论从coder还是运维的角度来看,这混搭增加了复杂度。
  • IE对http2以及SSE都支持的不好
  • HTTP/2 连接不确定性会永远保持连接,而websocket有onclose事件,对代码友好
  • 多个tab页windows页可能共用一个HTTP/2连接,你无法知道Server Push来自哪一个

vue3

  • Performance:性能更比Vue 2.0强。
  • Tree shaking support:可以将无用模块“剪辑”,仅打包需要的。
  • Composition API:组合API
  • Fragment, Teleport, Suspense:“碎片”,Teleport即Protal传送门,“悬念”
  • Better TypeScript support:更优秀的Ts支持
  • Custom Renderer API:暴露了自定义渲染API

性能:

编译模板的优化(PatchFlag:告知我们不光有TEXT变化,还有PROPS变化(id)。这样既跳出了virtual dom性能的瓶颈,又保留了可以手写render的灵活性。 等于是:既有react的灵活性,又有基于模板的性能保证。

事件监听缓存:cacheHandlers

开启cacheHandlers会自动生成并缓存一个内联函数,“神奇”的变为一个静态节点。 Ps:相当于React中useCallback自动化。

react native架构

React Native 新架构

  • JS thread。JS代码执行线程,负责逻辑层面的处理。Metro(打包工具)将React源码打包成一个单一JS文件(就是图中- JSBundle)。然后传给JS引擎执行,现在ios和android统一用的是JSC。
  • UI Thread(Main Thread/Native thread)。这个线程主要负责原生渲染(Native UI)和调用原生能力(Native Modules)比如蓝牙等。
  • Shadow Thread。 这个线程主要是创建Shadow Tree来模拟React结构树。Shadow Tree可以类似虚拟dom。RN使用Flexbox布局,但是原生是不支持,所以Yoga就是用来将Flexbox布局转换为原生平台的布局方式。

当我们写了类似下面的React源码。

<View style={{
        backgroundColor: 'pink',
        width: 200, 
        height: 200}}/> 

JS thread会先对其序列化,形成下面一条消息

UIManager.createView([343,"RCTView",31,{"backgroundColor":-16181,"width":200,"height":200}])

通过Bridge发到ShadowThread。Shadow Tread接收到这条信息后,先反序列化,形成Shadow tree,然后传给Yoga,形成原生布局信息。

接着又通过Bridge传给UI thread。

UI thread 拿到消息后,同样先反序列化,然后根据所给布局信息,进行绘制。

从上面过程可以看到三个线程的交互都是要通过Bridge,因此瓶颈也就在此。

Bridge三个特点:

  • 异步。这些消息队列是异步的,无法保证处理事件。
  • 序列化。通过JSON格式来传递消息,每次都要经历序列化和反序列化,开销很大。
  • 批处理。对Native调用进行排队,批量处理。

es6继承

阮一峰 es继承

子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。

ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。

20200525

event-loop

  • 每一轮 Event Loop 都会伴随着渲染吗?
  • requestAnimationFrame 在哪个阶段执行,在渲染前还是后?在 microTask 的前还是后?
  • requestIdleCallback 在哪个阶段执行?如何去执行?在渲染前还是后?在 microTask 的前还是后?
  • resize、scroll 这些事件是何时去派发的。

回流,重绘与合成的区别,如何优化

浏览器层合成

浏览器渲染流程

一般可以分为:构建 DOM 树、构建渲染树、布局、绘制、渲染层合成 几个步骤。

  • 构建 DOM 树:浏览器将 HTML 解析成树形结构的 DOM 树,一般来说,这个过程发生在页面初次加载,或页面 JavaScript 修改了节点结构的时候。

  • 构建渲染树:浏览器将 CSS 解析成树形结构的 CSSOM 树,再和 DOM 树合并成渲染树。

  • 布局(Layout):浏览器根据渲染树所体现的节点、各个节点的CSS定义以及它们的从属关系,计算出每个节点在屏幕中的位置。Web 页面中元素的布局是相对的,在页面元素位置、大小发生变化,往往会导致其他节点联动,需要重新计算布局,这时候的布局过程一般被称为回流(Reflow)。

  • 绘制(Paint):遍历渲染树,调用渲染器的 paint() 方法在屏幕上绘制出节点内容,本质上是一个像素填充的过程。这个过程也出现于回流或一些不影响布局的 CSS 修改引起的屏幕局部重画,这时候它被称为重绘(Repaint)。实际上,绘制过程是在多个层上完成的,这些层我们称为渲染层(RenderLayer)。

  • 渲染层合成(Composite):多个绘制后的渲染层按照恰当的重叠顺序进行合并,而后生成位图,最终通过显卡展示到屏幕上。

渲染层合成

在 DOM 树中每个节点都会对应一个渲染对象(RenderObject),当它们的渲染对象处于相同的坐标空间(z 轴空间)时,就会形成一个 RenderLayers,也就是渲染层。渲染层将保证页面元素以正确的顺序堆叠,这时候就会出现层合成(composite),从而正确处理透明元素和重叠元素的显示。
这个模型类似于 Photoshop 的图层模型,在 Photoshop 中,每个设计元素都是一个独立的图层,多个图层以恰当的顺序在 z 轴空间上叠加,最终构成一个完整的设计图。
对于有位置重叠的元素的页面,这个过程尤其重要,因为一旦图层的合并顺序出错,将会导致元素显示异常。

为什么react使用fiber 做时间分片不用generator

  1. 必须将每个函数包装在生成器中。
    这不仅增加了很多语法开销,而且还增加了任何现有实现中的运行时开销。
  2. 但是,最大的原因是生成器是有状态的。
    您无法在其中途恢复。
function* doWork(a, b, c) {
  var x = doExpensiveWorkA(a);
  yield;
  var y = x + doExpensiveWorkB(b);
  yield;
  var z = y + doExpensiveWorkC(c);
  return z;
}

如果要跨多个时间段执行此操作,则可以逐步执行。
但是,如果我已经完成doExpensiveWorkA(a)和doExpensiveWorkB(b)但没有完成doExpensiveWorkC(c)时获得了对B的更新,则我无法重用值x。

跳过具有不同b值的doExpensiveWorkB,但仍重用doExpensiveWorkA(a)的结果。

这对React很重要,因为我们做了大量的缓存。

可以将其添加为周围的层,这似乎是合理的,但实际上,使用generators并没有带来太多好处。

也有一些语言具有generators,这些generators是为具有此功能的更实用的用例而设计的。JS不是其中之一。

React 原生动态加载

react-lazy

React.lazy 接受一个函数作为参数,这个函数需要调用 import() 。它需要返回一个 Promise,该 Promise 需要 resolve 一个 defalut export 的 React 组件。

在控制台打印可以看到,React.lazy 方法返回的是一个 lazy 组件的对象,类型是 react.lazy,并且 lazy 组件具有 _status 属性,与 Promise 类似它具有 Pending、Resolved、Rejected 三个状态,分别代表组件的加载中、已加载、和加载失败三中状态。

需要注意的一点是,React.lazy 需要配合 Suspense 组件一起使用,在 Suspense 组件中渲染 React.lazy 异步加载的组件。如果单独使用 React.lazy,React 会给出错误提示。

Suspense 组件中,fallback 是一个必需的占位属性,如果没有这个属性的话也是会报错的。

在动态加载的组件资源比较小的情况下,会出现 fallback 组件一闪而过的的体验问题,如果不需要使用可以将 fallback 设置为 null。

当然针对这种场景,React 也提供了对应的解决方案,在 Concurrent Mode 模式下,给Suspense 组件设置 maxDuration 属性,当异步获取数据的时间大于 maxDuration 时间时,则展示 fallback 的内容,否则不展示。

 <Suspense 
   maxDuration={500} 
   fallback={<div>抱歉,请耐心等待 Loading...</div>}
 >
   <OtherComponent />
   <OtherComponentTwo />
</Suspense>

Suspense 可以包裹多个动态加载的组件,这也意味着在加载这两个组件的时候只会有一个 loading 层,因为 loading 的实现实际是 Suspense 这个父组件去完成的,当所有的子组件对象都 resolve 后,再去替换所有子组件。这样也就避免了出现多个 loading 的体验问题。所以 loading 一般不会针对某个子组件,而是针对整体的父组件做 loading 处理。

webpack动态加载

webpack 通过创建 script 标签来实现动态加载的,找出依赖对应的 chunk 信息,然后生成 script 标签来动态加载 chunk,每个 chunk 都有对应的状态:未加载 、 加载中、已加载 。

suspense

Error Boundaries

会了就不记录了

babel 原理

https://www.jianshu.com/p/e9b94b2d52e2

babel的转译过程也分为三个阶段:parsing、transforming、generating

ES6代码输入 ==》 babylon进行解析 ==》 得到AST
==》 plugin用babel-traverse对AST树进行遍历转译 ==》 得到新的AST树
==》 用babel-generator通过AST树生成ES5代码

webpack的优化配置(问到不会自然停) 然后loader plugin的写法 然后就再深点就源码(不会)
ES6 怎么转成ES5?
请简述 babel 的工作原理
为什么用ast啊

nextTick 微任务宏任务 双向数据绑定

然后Object.defineProperty 还有 proxy

打开页面的时候先出现无样式的页面 然后出现有样式的页面是怎么回事? 专业术语叫什么?原理

https://nextfe.com/how-chrome-compute-css/

20200429

CDN

cdn

webpack tree shaking

背景

ES6 模块的设计**是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。

// CommonJS模块
let { stat, exists, readfile } = require('fs');

// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

上面代码的实质是整体加载fs模块(即加载fs的所有方法),生成一个对象(_fs),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。

ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。

// ES6模块
import { stat, exists, readFile } from 'fs';

上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。

由于 ES6 模块是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。

如何达成

基于 ES6 的静态引用,tree shaking 通过扫描所有 ES6 的 export,找出被 import 的内容并添加到最终代码中。 webpack 的实现是把所有 import 标记为有使用/无使用两种,在后续压缩时进行区别处理。因为就如比喻所说,在放入烤箱(压缩混淆)前先剔除蛋壳(无使用的 import),只放入有用的蛋白蛋黄(有使用的 import)

Tree shaking 两步走

  1. webpack 负责对代码进行标记,把 import & export 标记为 3 类:
  • 所有 import 标记为 /* harmony import */

  • 被使用过的 export 标记为 /* harmony export ([type]) */,其中 [type] 和 webpack 内部有关,可能是 binding, immutable 等等。

  • 没被使用过的 import 标记为 /* unused harmony export [FuncName] */,其中 [FuncName] 为 export 的方法名称

  1. 之后在 Uglifyjs (或者其他类似的工具) 步骤进行代码精简,把没用的都删除。

Facebook发布了Hermes,这是安卓的JavaScript引擎,用于改进React Native

链接

fiber

react现状:一直以来react的动画卡顿都为人诟病,因js单线程,在执行逻辑,进行大型组件渲染的时候会占用浏览器主线程造成堵塞,这时候css dom树构建与用户输入等优先级比较高的任务都将被延迟搁置。
在现有React中,更新过程是同步的,因为JavaScript单线程的特点,每个同步任务不能耗时太长,不然就会让程序不会对其他输入作出相应,React的更新过程就是犯了这个禁忌,而React Fiber就是要改变现状。

React Fiber是对核心算法的一次重新实现

核心目标:扩大其适用性,包括动画,布局和手势。分为5个具体目标(后2个算送的):

  • 把可中断的工作拆分成小任务
  • 对正在做的工作调整优先次序、重做、复用上次(做了一半的)成果
  • 在父子任务之间从容切换(yield back and forth),以支持React执行过程中的布局刷新
  • 支持render()返回多个元素
  • 更好地支持error boundary

二. 关键特性

Fiber的关键特性如下:

  1. 增量渲染(把渲染任务拆分成块,匀到多帧)
  2. 更新时能够暂停,终止,复用渲染任务
  3. 给不同类型的更新赋予优先级
  4. 并发方面新的基础能力

把任务切片,运行完一段便将控制权交还到react负责任务调度的模块,再根据任务的优先级,继续运行后面的任务。所以会导致某些组件渲染到一半便会打断运行其他紧急,优先级更高的任务,运行完却不会继续之前终端的部分,而是重新开始,所以会出现生命周期多次调用的情况。

四. Fiber reconciler

reconcile过程分为2个阶段(phase):

  1. (可中断)render/reconciliation 通过构造workInProgress tree得出change
  2. (不可中断)commit 应用这些DOM change

在第一阶段Reconciliation Phase,React Fiber会找出需要更新哪些DOM,这个阶段是可以被打断的;但是到了第二阶段Commit Phase,那就一鼓作气把DOM更新完,绝不会被打断。

比如说,一个低优先级的任务A正在执行,已经调用了某个组件的componentWillUpdate函数,接下来发现自己的时间分片已经用完了,于是冒出水面,看看有没有紧急任务,哎呀,真的有一个紧急任务B,接下来React Fiber就会去执行这个紧急任务B,任务A虽然进行了一半,但是没办法,只能完全放弃,等到任务B全搞定之后,任务A重头来一遍,注意,是重头来一遍,不是从刚才中段的部分开始,也就是说,componentWillUpdate函数会被再调用一次。

如何做到优先级划分的

requestIdleCallback && requestAnimationFrame

requestIdleCallback: 在线程空闲时期调度执行低优先级函数(可能会隔几帧)
requestAnimationFrame: 在下一个动画帧调度前执行高优先级函数;

介绍事件代理/事件委托,解决的问题与手写其实现

事件委托

利用事件冒泡,只制定一个事件处理程序,就可以管理同一类型的所有事件。如父元素绑定事件所有子元素就可以触发,或把dom元素上绑定的事件统一绑定到document上。
DOM2.0模型将事件处理流程分为三个阶段:一、事件捕获阶段,二、事件目标阶段,三、事件起泡阶段。
事件捕获:当某个元素触发某个事件(如onclick),顶层对象document就会发出一个事件流,随着DOM树的节点向目标元素节点流去,直到到达事件真正发生的目标元素。在这个过程中,事件相应的监听函数是不会被触发的。 事件目标:当到达目标元素之后,执行目标元素该事件相应的处理函数。如果没有绑定监听函数,那就不执行。 事件起泡:从目标元素开始,往顶层元素传播。途中如果有节点绑定了相应的事件处理函数,这些函数都会被触发。

解决的问题

性能问题:javascript中添加到页面的事件处理程序的数量将影响到页面的整体运行性能,因为不断与dom节点进行交互,访问dom次数越多,会因此浏览器事件重绘与重排的次数也越多,会延长整个页面的交互就绪时间,这就是为什么性能优化的主要**之一就是dom操作的原因。
一个函数就是一个对象,是对象就要占用内存,一百个li就要占用一百个内存空间,而为父元素绑定一个事件就只占用一个事件的内存,且大大减少与dom的交互次数,提高性能。

说一说你对JS上下文栈和作用域链的理解?

JS执行上下文

执行上下文(Execution Contexts) 是 ECMAScript 代码 运行时(runtime) 的上下文环境。

执行上下文类型分为:

  • 全局执行上下文
  • 函数执行上下文
  • eval函数执行上下文(不被推荐)

执行上下文创建过程中,需要做以下几件事:

  1. 创建变量对象:首先初始化函数的参数arguments,提升函数声明和变量声明。
  2. 创建作用域链(Scope Chain):在执行期上下文的创建阶段,作用域链是在变量对象之后创建的。
  3. 确定this的值,即 ResolveThisBinding

作用域

作用域负责收集和维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。—— 摘录自《你不知道的JavaScript》(上卷)

作用域分为:

  • 全局作用域
  • 函数作用域
  • 块级作用域

JS执行上下文栈(后面简称执行栈)

执行栈,也叫做调用栈,具有 LIFO (后进先出) 结构,用于存储在代码执行期间创建的所有执行上下文。

作用域链

作用域 负责收集和维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。-- 《你不知道的JavaScript(上卷)》
从上面两个话题,我们可以知道,除 全局作用域(global scope) 外,每个作用域始终连接到其背后的一个或多个作用域,从而形成 作用域链(scope chain) 。全局作用域(global scope) 没有任何父级,这也是有意义的,因为它位于层次结构的顶部。

事件轮询event loop

event loop

JS的单线程:
在 JS 运行的时候可能会阻止 UI 渲染,这说明了两个线程是互斥的。这其中的原因是因为 JS 可以修改 DOM,如果在 JS 执行的时候 UI 线程还在工作,就可能导致不能安全的渲染 UI。这其实也是一个单线程的好处,得益于 JS 是单线程运行的,可以达到节省内存,节约上下文切换时间,没有锁的问题的好处。当然前面两点在服务端中更容易体现,对于锁的问题,形象的来说就是当我读取一个数字 15 的时候,同时有两个操作对数字进行了加减,这时候结果就出现了错误。解决这个问题也不难,只需要在读取的时候加锁,直到读取完毕之前都不能进行写入操作。

实当遇到异步的代码时,会被挂起并在需要执行的时候加入到 Task(有多种 Task) 队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说 JS 中的异步还是同步行为。

不同的任务源会被分配到不同的 Task 队列中,任务源可以分为 微任务(microtask) 和 宏任务(macrotask)。在 ES6 规范中,microtask 称为 jobs,macrotask 称为 task。下面来看以下代码的执行顺序:

  • 首先执行同步代码,这属于宏任务
  • 当执行完所有同步代码后,执行栈为空,查询是否有异步代码需要执行
  • 执行所有微任务
  • 当执行完所有微任务后,如有必要会渲染页面
  • 然后开始下一轮 Event Loop,执行宏任务中的异步代码,也就是 setTimeout 中的回调函数

微任务包括 process.nextTick ,promise ,MutationObserver,其中 process.nextTick 为 Node 独有。

宏任务包括 script , setTimeout ,setInterval ,setImmediate ,I/O ,UI rendering。

setTimeout 误差

因为setTimeout是异步宏任务,如果执行栈中的执行所用的时间超过了定时器的间隔时间,根据上述的轮询机制,便会出现误差。

setTimeout(function () {
	console.log('biubiu');
}, 1000);

如果定时器下面的函数执行要 5秒钟,那么定时器里的log 则需要 5秒之后再大圆,函数占用了当前 执行栈 ,要等执行栈执行完毕后再去读取 微任务(microtask),等 微任务(microtask) 完成,这个时候才会去读取 宏任务(macrotask) 里面的 setTimeout 回调函数执行。setInterval 同理,例如每3秒放入宏任务,也要等到执行栈的完成。

还有一种情况如下:

setTimeout(function() {
    console.log('嘤嘤嘤');
}, 0);

因为 定时器 最小 delay 是 4毫秒,所以小于这个数字,即使 执行栈 已空,依然不会按设置好的 delay 去执行。

HTTP相关问题

参考:

HTTP有哪些方法?

  • HTTP1.0定义了三种请求方法: GET, POST 和 HEAD方法
  • HTTP1.1新增了五种请求方法:OPTIONS, PUT, DELETE, TRACE 和 CONNECT

这些方法的具体作用是什么?

  • GET: 通常用于请求服务器发送某些资源
  • HEAD: 请求资源的头部信息, 并且这些头部与 HTTP GET 方法请求时返回的一致. 该请求方法的一个使用场景是在下载一个大文件前先获取其大小再决定是否要下载, 以此可以节约带宽资源
  • OPTIONS: 用于获取目的资源所支持的通信选项
  • POST: 发送数据给服务器
  • PUT: 用于新增资源或者使用请求中的有效负载替换目标资源的表现形式
  • DELETE: 用于删除指定的资源
  • PATCH: 用于对资源进行部分修改
  • CONNECT: HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器
  • TRACE: 回显服务器收到的请求,主要用于测试或诊断

GET和POST有什么区别?

  1. get请求数据会放在url上,post则放进请求体中
  2. 数据类型不同:get只允许ascll字符,而post 无限制
  3. 安全性来说post因为数据在请求体中会更安全,get的请求数据在url中容易通过缓存,历史记录获取到。
  4. 幂等,根据HTTP规范,GET用于信息获取,而且应该是安全的和幂等的。幂等的意味着对同一URL的多个请求应该返回同样的结果,POST表示可能修改变服务器上的资源的请求。

PUT和POST

根本区别是put是幂等,post是非幂等。通常情况下,PUT的URI指向是具体单一资源,而POST可以指向资源集合。

PUT和PATCH

PUT和PATCH都是更新资源,而PATCH用来对已知资源进行局部更新。

http的请求报文是什么样的?

包括请求行,请求头,空行,请求体

  • 请求行包括:请求方法,url,http版本协议等字段,空哥分隔开。
  • 请求头:请求头部由关键字/值对组成,每行一对,关键字和值用英文冒号“:”分隔
  1. User-Agent: 产生请求的浏览器类型
  2. Accept: 客户端可识别的内容类型列表
  3. Host: 请求的主机名,允许多个域名同处一个ip地址,即虚拟主机。
  • 请求体:post put等请求携带的数据

http的响应报文是什么样的?

请求报文有4部分组成:响应行,响应头,空行,响应体

两数之和

给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。

const getAddArray = (arr, target) =>
  arr.reduce((res, cur, i) => {
    return arr.includes(target - cur)
      ? [...new Set([...res, cur, target - cur])]
      : res;
  }, []);

let array = [1, 2, 3, 4, 0];
console.log(getAddArray(array, 3));

项目异常捕获

crm是一个多tag页面的SPA项目,打包发布完,用户在没有刷新页面的情况下使用会偶尔出现白屏或一直loading无响应,控制台出现load chunk fail error的报错,分析之后是因为切换页面会发起对对应页面分包代码资源的请求,而当前旧资源已经被清除了。

我引入了ErrorBoundary组件,通过新增的staic getderivedstatefromerror和componentDidcatch生命周期显示备用的样式fallback UI。当我用staic getderivedstatefromerror获取error堆栈信息,设置state中isError为true显示对应友好的报错页面,componentDidcatch进行错误上报,心满意足地觉得毫无破绽的时候,发现其实是没办法捕获到加载分包失败的错误的。

异常捕获是在渲染过程中workloop进行捕获的,而接口报错和事件处理器onClick对应回调内部的js报错则无法捕捉。

这个时候我们可以根据加载分包的loading组件来设置定时,若加载超过一定限制则报错,通过全局状态库保存error值,errorboundry组件获取值进行错误显示,显示替补样式,提醒用户进行刷新。

  const [isTimeout, setIsTimeout] = useState(false);
  useEffect(() => {
    let timer: NodeJS.Timeout;
    if (error) {
      // @ts-ignore
      clearInterval(timer);
    }
    if (!isTimeout) {
      timer = setTimeout(() => {
        setIsTimeout(true);
      }, 4000);
    }
    return () => {
      clearTimeout(timer);
    };
  }, [isTimeout, error]);

但又出现了新问题,我们多tag模式下刷新的话导致其他页面一起刷新,又要重新打开页面录入一堆信息,是你恼火不恼火?那么能不能刷新单个页面吗,重新请求当前页面的分包呢?但是我发现请求失败了如何重新请求分包呢?

我们用的是umi框架,进入源码可以看到代码分割的库是loadable,又是熟悉的老朋友,看一下代码分割实现的源码

umi/packages/runtime/src/dynamic/loadable.js

  const LoadableComponent = (props, ref) => {
    init();

    const context = useContext(LoadableContext);
    const state = useSubscription(subscription);

    useImperativeHandle(ref, () => ({
      retry: subscription.retry,
    }));

    if (context && Array.isArray(opts.modules)) {
      opts.modules.forEach((moduleName) => {
        context(moduleName);
      });
    }

    if (state.loading || state.error) {
      if (process.env.NODE_ENV === 'development' && state.error) {
        console.error(`[@umijs/runtime] load component failed`, state.error);
      }
      return createElement(opts.loading, {
        isLoading: state.loading,
        pastDelay: state.pastDelay,
        timedOut: state.timedOut,
        error: state.error,
        retry: subscription.retry,
      });
    } else if (state.loaded) {
      return opts.render(state.loaded, props);
    } else {
      return null;
    }
  };

  const LoadableComponentWithRef = forwardRef(LoadableComponent);
  // add static method in React.forwardRef
  // https://github.com/facebook/react/issues/17830
  LoadableComponentWithRef.preload = () => init();
  LoadableComponentWithRef.displayName = 'LoadableComponent';

  return LoadableComponentWithRef;
}

原来已经暴露了error信息和retry方法,利用retry方法我们可以提供重新刷新当前页面的能力,来重新请求当前分包。

diff算法(vue react

聊一聊Diff算法

Vue2的核心Diff算法采用了双端比较的算法,同时从新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作

谈谈你对闭包的理解及其优缺点

可以访问另一个函数私有变量的函数,如函数作用域向外暴露返回一个可以访问内部私有变量的匿名函数时。
“当函数可以记住并访问所在的词法作用域,即函数是在当前作用域之外执行,这就产生了闭包。”

优点:可以封装一个不能被轻易修改的局部变量,避免变量冲突,实现持久化。
缺点:驻留在内存中容易造成内存泄漏,增大内存使用量。

HTTP

五大特点=》支持客户/服务器模式;2、简单快速;3、灵活;4、无连接;5、无状态。

无连接

每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。

Keep-Alive 功能使客户端到服务器端的连接持续有效,当出现对服务器的后继请求时,Keep-Alive 功能避免了建立或者重新建立连接。

参考:

HTTP有哪些方法?

  • HTTP1.0定义了三种请求方法: GET, POST 和 HEAD方法
  • HTTP1.1新增了五种请求方法:OPTIONS, PUT, DELETE, TRACE 和 CONNECT

这些方法的具体作用是什么?

  • GET: 通常用于请求服务器发送某些资源
  • HEAD: 请求资源的头部信息, 并且这些头部与 HTTP GET 方法请求时返回的一致. 该请求方法的一个使用场景是在下载一个大文件前先获取其大小再决定是否要下载, 以此可以节约带宽资源
  • OPTIONS: 用于获取目的资源所支持的通信选项
  • POST: 发送数据给服务器
  • PUT: 用于新增资源或者使用请求中的有效负载替换目标资源的表现形式
  • DELETE: 用于删除指定的资源
  • PATCH: 用于对资源进行部分修改
  • CONNECT: HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器
  • TRACE: 回显服务器收到的请求,主要用于测试或诊断

GET和POST有什么区别?

  1. get请求数据会放在url上,post则放进请求体中
  2. 数据类型不同:get只允许ascll字符,而post 无限制
  3. 安全性来说post因为数据在请求体中会更安全,get的请求数据在url中容易通过缓存,历史记录获取到。
  4. 幂等,根据HTTP规范,GET用于信息获取,而且应该是安全的和幂等的。幂等的意味着对同一URL的多个请求应该返回同样的结果,POST表示可能修改变服务器上的资源的请求。

PUT和POST

根本区别是put是幂等,post是非幂等。通常情况下,PUT的URI指向是具体单一资源,而POST可以指向资源集合。

PUT和PATCH

PUT和PATCH都是更新资源,而PATCH用来对已知资源进行局部更新。

http的请求报文是什么样的?

包括请求行,请求头,空行,请求体

  • 请求行包括:请求方法,url,http版本协议等字段,空哥分隔开。
  • 请求头:请求头部由关键字/值对组成,每行一对,关键字和值用英文冒号“:”分隔
  1. User-Agent: 产生请求的浏览器类型
  2. Accept: 客户端可识别的内容类型列表
  3. Host: 请求的主机名,允许多个域名同处一个ip地址,即虚拟主机。
  • 请求体:post put等请求携带的数据

http的响应报文是什么样的?

请求报文有4部分组成:响应行,响应头,空行,响应体

三次握手

SYN:主动建立连接进行确认的标示,喊话我开始连接了
ACK:确认用的标示
seq:初始序号,随机生成标示,其后对面那个b会返回一个确认号ack对该值进行+1,进行来回交锋

  1. 开始第一次握手:客户端发送连接请求报文段(SYN=1,seq=x )到服务器,自己进入 SYN-SENT(同步已发送)状态。
  2. 第二次:服务器接收到了连接请求报文段后,如果自己同意进行连接,则要返回确认报文段(SYN=1,ACK=1, ack number= x+1,seq=y),服务器同步进入SYN-RCVD状态。
  3. 第三次:客户端接收到,还需再发一次确认报文段(ACK=1, ack= y+1,seq=x+1)TCP连接已经建立,A进入ESTABLISHED

前端模块化: AMD,CMD,CommonJS,ES6

AMD和require.js

异步加载,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

CMD和sea.js

AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。此规范其实是在sea.js推广过程中产生的。

/** AMD写法 **/
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) { 
     // 等于在最前面声明并初始化了要用到的所有模块
    a.doSomething();
    if (false) {
        // 即便没用到某个模块 b,但 b 还是提前执行了
        b.doSomething()
    } 
});

/** CMD写法 **/
define(function(require, exports, module) {
    var a = require('./a'); //在需要时申明
    a.doSomething();
    if (false) {
        var b = require('./b');
        b.doSomething();
    }
});

CommonJS

同步加载,nodeJs服务端中模块文件都存在本地磁盘,读取非常快,不会有问题。

ES6

ES6 模块的设计**是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。

ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。

由于 ES6 模块是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。

区别

  1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
  2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
    https://segmentfault.com/a/1190000020388889

如何实现异常监控

先发散思维,列出自己可以想到的方法
react16在新的fiber架构下,新增生命周期componentDidcatch和static getDerivedStateFromError,可以捕捉事件异常
getDerivedStateFromError是协调阶段的生命周期,适合进行显示对应降级后UI的变更操作,而如记录错误信息会产生副作用的操作适合放在处于commit阶段的getDerivedStateFromError,进行错误收集后请求接口,上传到对应的服务器。
看了最新的sentry版本也是有对应的error boundry组件,相信也是运用了对应的生命周期
那么在16提供这个生命周期之前是怎么捕获的呢?现在的hook也同样不具备生命周期,该如何解决?

浅谈Sentry前端监控原理

window.onerror劫持

每当代码在runtime时发生错误时,JavaScript引擎就会抛出一个Error对象,并且触发window.onerror函数。

Sentry对window.onerror函数进行了改写,在这里实现了错误监控的逻辑,添加了很多运行时信息帮助进行错误定位,对错误处理进行跨浏览器的兼容等等。

在这里Sentry使用了TraceKit来帮助它劫持window.onerror函数。TraceKit主要是用来进行抹平各浏览器之间的差异,使得错误处理的逻辑统一。

监听unhandledrejection事件

在我们使用Promise的时候,如果发生错误而我们没有去catch的话,window.onerror是不能监控到这个错误的。但是这个时候,JavaScript引擎会触发unhandledrejection事件,只要我们监听这个事件,那么就能够监控到Promise产生的错误。

走进React Fiber 架构

本文重点:介绍React重构的起因和目的,理解Fiber tree单向链表结构中各属性含义,梳理调度过程和核心实现手段,深入新的生命周期,hooks,suspense,异常捕获等特性的用法和原理。

喜欢的就点个赞吧️,希望跟大家在枯燥的源码中发掘学习的乐趣,一起分享进步。

当react刚推出的时候,最具革命性的特性就是虚拟dom,因为这大大降低了应用开发的难度,相比较以往告诉浏览器我需要怎么更新我的ui,现在我们只需要告诉react我应用ui的下个状态是怎么样的,react会帮我们自动处理两者之间的所有事宜。

这让我们可以从属性操作、事件处理和手动 DOM 更新这些在构建应用程序时必要的操作中解放出来。宿主树的概念让这个优秀的框架有无限的可能性,react native便是其在原生移动应用中伟大的实现。

但在享受舒适开发体验的同时,有一些疑问一直萦绕在我们脑海中:

  • 是什么导致了react用户交互、动画频繁卡顿
  • 如何视线优雅的异常处理,进行异常捕获和备用ui渲染
  • 如何更好实现组件的复用和状态管理

这究竟是人性的扭曲,还是道德的沦丧 /狗头

Fiber能否给我们答案,又将带给我们什么惊喜,卷起一波新的浪潮,欢迎收看《走进Fiber》

那么,简而言之,React Fiber是什么?

Fiber是对React核心算法的重构,2年重构的产物就是Fiber reconciler。

react协调是什么

协调是react中重要的一部分,其中包含了如何对新旧树差异进行比较以达到仅更新差异的部分。

现在的react经过重构后Reconciliation和Rendering被分为两个不同的阶段。

  • reconciler协调阶段:当组件次初始化和其后的状态更新中,React会创建两颗不相同的虚拟树,React 需要基于这两棵树之间的差别来判断如何有效率的更新 UI 以保证当前 UI 与最新的树保持同步,计算树哪些部分需要更新。
  • renderer阶段:渲染器负责将拿到的虚拟组件树信息,根据其对应环境真实地更新渲染到应用中。有兴趣的朋友可以看一下dan自己的博客中的文章=》运行时的react=》渲染器,介绍了react的Renderer渲染器如react-dom和react native等,其可以根据不同的主环境来生成不同的实例。

为什么要重写协调

动画是指由许多帧静止的画面,以一定的速度(如每秒16张)连续播放时,肉眼因视觉残象产生错觉,而误以为画面活动的作品。——维基百科

老一辈人常常把电影称为“移动的画”,我们小时候看的手翻书就是快速翻动的一页页画,其本质上实现原理跟动画是一样的。

帧:在动画过程中,每一幅静止画面即为一“帧”;
帧率:是用于测量显示帧数的量度,测量单位为“每秒显示帧数”(Frame per Second,FPS)或“赫兹”;
帧时长:即每一幅静止画面的停留时间,单位一般是ms(毫秒);
丢帧:在帧率固定的动画中,某一帧的时长远高于平均帧时长,导致其后续数帧被挤压而丢失的现象;

当前大部分笔记本电脑和手机的常见帧率为60hz,即一秒显示60帧的画面,一帧停留的时间为16.7ms(1000/60≈16.7),这就留给了开发者和UI系统大约16.67ms来完成生成一张静态图片(帧)所需要的所有工作。如果在这分派的16.67ms之内没有能够完成这些工作,就会引发‘丢帧’的后果,使界面表现的不够流畅。

浏览器中的GUI渲染线程和JS引擎线程

在浏览器中GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。


浏览器拥挤的主线程

React16 推出Fiber之前协调算法是Stack Reconciler,即递归遍历所有的 Virtual DOM 节点执行Diff算法,一旦开始便无法中断,直到整颗虚拟dom树构建完成后才会释放主线程,因其JavaScript单线程的特点,若当下组件具有复杂的嵌套和逻辑处理,diff便会堵塞UI进程,使动画和交互等优先级相对较高的任务无法立即得到处理,造成页面卡顿掉帧,影响用户体验。

16年在 facebook 上 Seb 正式提到了 Fiber 这个概念,解释为什么要重写框架:

Once you have each stack frame as an object on the heap you can do clever things like reusing it during future updates and yielding to the event loop without losing any of your currently in progress data.
一旦将每个堆栈帧作为堆上的对象,您就可以做一些聪明的事情,例如在将来的更新中重用它并暂停于事件循环,而不会丢失任何当前正在进行的数据。

我们来做一个实验

function randomHexColor() {
  return (
    "#" + ("0000" + ((Math.random() * 0x1000000) << 0).toString(16)).substr(-6)
  );
}

var root = document.getElementById("root");

// 一次性遍历100000次
function a() {
  setTimeout(function() {
    var k = 0;
    for (var i = 0; i < 10000; i++) {
      k += new Date() - 0;
      var el = document.createElement("div");
      el.innerHTML = k;
      root.appendChild(el);
      el.style.cssText = `background:${randomHexColor()};height:40px`;
    }
  }, 1000);
}

// 每次只操作100个节点,共100次
function b() {
  setTimeout(function() {
    function loop(n) {
      var k = 0;
      console.log(n);
      for (var i = 0; i < 100; i++) {
        k += new Date() - 0;
        var el = document.createElement("div");
        el.innerHTML = k;
        root.appendChild(el);
        el.style.cssText = `background:${randomHexColor()};height:40px`;
      }
      if (n) {
        setTimeout(function() {
          loop(n - 1);
        }, 40);
      }
    }
    loop(100);
  }, 1000);
}

a执行性能截图:掉帧严重,普遍fps为1139.6ms

b执行性能截图: fps处于15ms~19ms

究其原因是因为浏览器的主线程需要处理GUI描绘,时间器处理,事件处理,JS执行,远程资源加载等,当做某件事,只有将它做完才能做下一件事。如果有足够的时间,浏览器是会对我们的代码进行编译优化(JIT)及进行热代码优化,一些DOM操作,内部也会对reflow进行处理。reflow是一个性能黑洞,很可能让页面的大多数元素进行重新布局。

而作为一只有梦想的前端菜🐤,为用户爸爸呈现最好的交互体验是我们义不容辞的责任,把困难扛在肩上,让我们see see react是如何解决以上的问题。

Fiber你是个啥(第四音

那么我们先看看作为看看解决方案的Fiber是什么,然后在分析为什么它能解决以上问题。

定义:

  1. react Reconciliation协调核心算法的一次重新实现
  2. 虚拟堆栈帧
  3. 具备扁平化的链表数据存储结构的js对象,Reconciliation阶段所能拆分的最小工作单元

针对其定义我们来进行拓展:

虚拟堆栈帧:

Andrew Clark的React Fiber体系文档很好地解释了Fiber实现背后的想法,我在这里引用一下:

Fiber是堆栈的重新实现,专门用于React组件。
您可以将单个Fiber视为虚拟堆栈框架。
重新实现堆栈的优点是,您可以将堆栈帧保留在内存中,并根据需要(以及在任何时候)执行它们。
这对于实现调度的目标至关重要。

JavaScript的执行模型:call stack

JavaScript原生的执行模型:通过调用栈来管理函数执行状态。
其中每个栈帧表示一个工作单元(a unit of work),存储了函数调用的返回指针、当前函数、调用参数、局部变量等信息。
因为JavaScript的执行栈是由引擎管理的,执行栈一旦开始,就会一直执行,直到执行栈清空。无法按需中止。

react以往的渲染就是使用原生执行栈来管理组件树的递归渲染,当其层次较深component不断递归子节点,无法被打断就会导致主线程堵塞ui卡顿。

可控的调用栈

所以理想状况下reconciliation的过程应该是像下图所示一样,将繁重的任务划分成一个个小的工作单元,做完后能够“喘口气儿”。我们需要一种增量渲染的调度,Fiber就是重新实现一个堆栈帧的调度,这个堆栈帧可以按照自己的调度算法执行他们。另外由于这些堆栈是可将可中断的任务拆分成多个子任务,通过按照优先级来自由调度子任务,分段更新,从而将之前的同步渲染改为异步渲染。

它的特性就是时间分片(time slicing)和暂停(supense)。

具备扁平化的链表数据存储结构的js对象:

fiber是一个js对象,fiber的创建是通过React元素来创建的,在整个React构建的虚拟DOM树中,每一个元素都对应有一个fiber,从而构建了一棵fiber树,每个fiber不仅仅包含每个元素的信息,还包含更多的信息,以方便Scheduler来进行调度。

让我们看一下fiber的结构

type Fiber = {|
  // 标记不同的组件类型
  //export const FunctionComponent = 0;
  //export const ClassComponent = 1;
  //export const HostRoot = 3; 可以理解为这个fiber是fiber树的根节点,根节点可以嵌套在子树中
  //export const Fragment = 7;
  //export const SuspenseComponent = 13;
  //export const MemoComponent = 14;
  //export const LazyComponent = 16;
  tag: WorkTag,

  // ReactElement里面的key
  // 唯一标示。我们在写React的时候如果出现列表式的时候,需要制定key,这key就是对应元素的key。
  key: null | string,

  // ReactElement.type,也就是我们调用`createElement`的第一个参数
  elementType: any,

  // The resolved function/class/ associated with this fiber.
  // 异步组件resolved之后返回的内容,一般是`function`或者`class`
  type: any,

  // The local state associated with this fiber.
  // 跟当前Fiber相关本地状态(比如浏览器环境就是DOM节点)
  // 当前组件实例的引用
  stateNode: any,

  // 指向他在Fiber节点树中的`parent`,用来在处理完这个节点之后向上返回
  return: Fiber | null,

  // 单链表树结构
  // 指向自己的第一个子节点
  child: Fiber | null,
  // 指向自己的兄弟结构
  // 兄弟节点的return指向同一个父节点
  sibling: Fiber | null,
  index: number,

  // ref属性
  ref: null | (((handle: mixed) => void) & {_stringRef: ?string}) | RefObject,

  // 新的变动带来的新的props
  pendingProps: any, 
  // 上一次渲染完成之后的props
  memoizedProps: any,

  // 该Fiber对应的组件产生的Update会存放在这个队列里面
  updateQueue: UpdateQueue<any> | null,

  // 上一次渲染的时候的state
  // 用来存放某个组件内所有的 Hook 状态
  memoizedState: any,

  // 一个列表,存放这个Fiber依赖的context
  firstContextDependency: ContextDependency<mixed> | null,

  // 用来描述当前Fiber和他子树的`Bitfield`
  // 共存的模式表示这个子树是否默认是异步渲染的
  // Fiber被创建的时候他会继承父Fiber
  // 其他的标识也可以在创建的时候被设置
  // 但是在创建之后不应该再被修改,特别是他的子Fiber创建之前
  //用来描述fiber是处于何种模式。用二进制位来表示(bitfield),后面通过与来看两者是否相同//这个字段其实是一个数字.实现定义了一下四种//NoContext: 0b000->0//AsyncMode: 0b001->1//StrictMode: 0b010->2//ProfileMode: 0b100->4
  mode: TypeOfMode,

  // Effect
  // 用来记录Side Effect具体的执行的工作的类型:比如Placement,Update等等
  effectTag: SideEffectTag,

  // 单链表用来快速查找下一个side effect
  nextEffect: Fiber | null,

  // 子树中第一个side effect
  firstEffect: Fiber | null,
  // 子树中最后一个side effect
  lastEffect: Fiber | null,

  // 代表任务在未来的哪个时间点应该被完成
  // 不包括他的子树产生的任务
  // 通过这个参数也可以知道是否还有等待暂停的变更、没有完成变更。
  // 这个参数一般是UpdateQueue中最长过期时间的Update相同,如果有Update的话。
  expirationTime: ExpirationTime,

  // 快速确定子树中是否有不在等待的变化
  childExpirationTime: ExpirationTime,

  //当前fiber对应的工作中的Fiber。
  // 在Fiber树更新的过程中,每个Fiber都会有一个跟其对应的Fiber
  // 我们称他为 current <==> workInProgress
  // 在渲染完成之后他们会交换位置
  alternate: Fiber | null,
  ...
|};

ReactWorkTags组件类型

链表结构


fiber中最为重要的是return、child、sibling指针,连接父子兄弟节点以构成一颗单链表fiber树,其扁平化的单链表结构的特点将以往递归遍历改为了循环遍历,实现深度优先遍历。

React16特别青睐于链表结构,链表在内存里不是连续的,动态分配,增删方便,轻量化,对异步友好

current与workInProgress

current树:React 在 render 第一次渲染时,会通过 React.createElement 创建一颗 Element 树,可以称之为 Virtual DOM Tree,由于要记录上下文信息,加入了 Fiber,每一个 Element 会对应一个 Fiber Node,将 Fiber Node 链接起来的结构成为 Fiber Tree。它反映了用于渲染 UI 和映射应用状态。这棵树通常被称为 current 树(当前树,记录当前页面的状态)。

workInProgress树:当React经过current当前树时,对于每一个先存在的fiber节点,它都会创建一个替代(alternate)节点,这些节点组成了workInProgress树。这个节点是使用render方法返回的React元素的数据创建的。一旦更新处理完以及所有相关工作完成,React就有一颗替代树来准备刷新屏幕。一旦这颗workInProgress树渲染(render)在屏幕上,它便成了当前树。下次进来会把current状态复制到WIP上,进行交互复用,而不用每次更新的时候都创建一个新的对象,消耗性能。这种同时缓存两棵树进行引用替换的技术被称为双缓冲技术

function createWorkInProgress(current, ...) {
  let workInProgress = current.alternate;
  if (workInProgress === null) {
    workInProgress = createFiber(...);
  }
  ...
  workInProgress.alternate = current;
  current.alternate = workInProgress;
  ...
  return workInProgress;
}


alternate fiber可以理解为一个fiber版本池,用于交替记录组件更新(切分任务后变成多阶段更新)过程中fiber的更新,因为在组件更新的各阶段,更新前及更新过程中fiber状态并不一致,在需要恢复时(如发生冲突),即可使用另一者直接回退至上一版本fiber。

Dan在Beyond React 16演讲中用了一个非常恰当的比喻,那就是Git 功能分支,你可以将 WIP 树想象成从旧树中 Fork 出来的功能分支,你在这新分支中添加或移除特性,即使是操作失误也不会影响旧的分支。当你这个分支经过了测试和完善,就可以合并到旧分支,将其替换掉。

Update

  • 用于记录组件状态的改变
  • 存放于fiber的updateQueue里面
  • 多个update同时存在

比如设置三个setState(),React是不会立即更新的,而是放到UpdateQueue中,再去更新

ps: setState一直有人疑问为啥不是同步,将 setState() 视为请求而不是立即更新组件的命令。为了更好的感知性能,React 会延迟调用它,然后通过一次传递更新多个组件。React 并不会保证 state 的变更会立即生效。

export function createUpdate(
  expirationTime: ExpirationTime,
  suspenseConfig: null | SuspenseConfig,
): Update<*> {
  let update: Update<*> = {
    //任务过期事件
    //在创建每个更新的时候,需要设定过期时间,过期时间也就是优先级。过期时间越长,就表示优先级越低。
    expirationTime,
    // suspense的配置
    suspenseConfig,

  // export const UpdateState = 0; 表示更新State
  // export const ReplaceState = 1; 表示替换State
  // export const ForceUpdate = 2; 强制更新
  // export const CaptureUpdate = 3; 捕获更新(发生异常错误的时候发生)
  // 指定更新的类型,值为以上几种
    tag: UpdateState,
    // 更新内容,比如`setState`接收的第一个参数
    payload: null,
    // 更新完成后的回调,`setState`,`render`都有
    callback: null,

    // 指向下一个update
    // 单链表update queue通过 next串联
    next: null,
    
    // 下一个side effect
    // 最新源码被抛弃 next替换
    //nextEffect: null,
  };
  if (__DEV__) {
    update.priority = getCurrentPriorityLevel();
  }
  return update;
}

UpdateQueue

//创建更新队列
export function createUpdateQueue<State>(baseState: State): UpdateQueue<State> {
  const queue: UpdateQueue<State> = {
    //应用更新后的state
    baseState,
    //队列中的第一个update
    firstUpdate: null,
    //队列中的最后一个update
    lastUpdate: null,
     //队列中第一个捕获类型的update
    firstCapturedUpdate: null,
    //队列中最后一个捕获类型的update
    lastCapturedUpdate: null,
    //第一个side effect
    firstEffect: null,
    //最后一个side effect
    lastEffect: null,
    firstCapturedEffect: null,
    lastCapturedEffect: null,
  };
  return queue;
}

update中的payload:通常我们现在在调用setState传入的是一个对象,但在使用fiber conciler时,必须传入一个函数,函数的返回值是要更新的state。react从很早的版本就开始支持这种写法了,不过通常没有人用。在之后的react版本中,可能会废弃直接传入对象的写法。

setState({}, callback); // stack conciler
setState(() => { return {} }, callback); // fiber conciler

ReactUpdateQueue源码

Updater

每个组件都会有一个Updater对象,它的用处就是把组件元素更新和对应的fiber关联起来。监听组件元素的更新,并把对应的更新放入该元素对应的fiber的UpdateQueue里面,并且调用ScheduleWork方法,把最新的fiber让scheduler去调度工作。

const classComponentUpdater = {
  isMounted,
  enqueueSetState(inst, payload, callback) {
    const fiber = getInstance(inst);
    const currentTime = requestCurrentTimeForUpdate();
    const suspenseConfig = requestCurrentSuspenseConfig();
    const expirationTime = computeExpirationForFiber(
      currentTime,
      fiber,
      suspenseConfig,
    );

    const update = createUpdate(expirationTime, suspenseConfig);
    update.payload = payload;
    if (callback !== undefined && callback !== null) {
      if (__DEV__) {
        warnOnInvalidCallback(callback, 'setState');
      }
      update.callback = callback;
    }

    enqueueUpdate(fiber, update);
    scheduleWork(fiber, expirationTime);
  },
  enqueueReplaceState(inst, payload, callback) {
    //一样的代码
    //...
    update.tag = ReplaceState;
    //...
  },
  enqueueForceUpdate(inst, callback) {
    //一样的代码
    //...
    update.tag = ForceUpdate;
    //...
  },
};

ReactUpdateQueue=>classComponentUpdater

Effect list

Side Effects:我们可以将React中的一个组件视为一个使用state和props来计算UI的函数。每个其他活动,如改变DOM或调用生命周期方法,都应该被认为是side-effects,react文档中是这样描述的side-effects的:

You’ve likely performed data fetching, subscriptions, or manually changing the DOM 的from React components before. We call these operations “side effects” (or “effects” for short) because they can affect other components and can’t be done during rendering.

React能够非常快速地更新,并且为了实现高性能,它采用了一些有趣的技术。其中之一是构建带有side-effects的fiber节点的线性列表,其具有快速迭代的效果。迭代线性列表比树快得多,并且没有必要在没有side effects的节点上花费时间。

每个fiber节点都可以具有与之相关的effects, 通过fiber节点中的effectTag字段表示。


此列表的目标是标记具有DOM更新或与其关联的其他effects的节点,此列表是WIP tree的子集,并使用nextEffect属性,而不是current和workInProgress树中使用的child属性进行链接。

How it work

核心目标

  • 把可中断的工作拆分成多个小任务
  • 为不同类型的更新分配任务优先级
  • 更新时能够暂停,终止,复用渲染任务

更新过程概述

我们先看看其Fiber的更新过程,然后再针对过程中的核心技术进行展开。

Reconciliation分为两个阶段:reconciliation 和 commit

reconciliation


从图中可以看到,可以把reconciler阶段分为三部分,分别以红线划分。简单的概括下三部分的工作:

  1. 第一部分从 ReactDOM.render() 方法开始,把接收的React Element转换为Fiber节点,并为其设置优先级,记录update等。这部分主要是一些数据方面的准备工作。
  2. 第二部分主要是三个函数:scheduleWork、requestWork、performWork,即安排工作、申请工作、正式工作三部曲。React 16 新增的异步调用的功能则在这部分实现。
  3. 第三部分是一个大循环,遍历所有的Fiber节点,通过Diff算法计算所有更新工作,产出 EffectList 给到commit阶段使用。这部分的核心是 beginWork 函数。

commit阶段

这个阶段主要做的工作拿到reconciliation阶段产出的所有更新工作,提交这些工作并调用渲染模块(react-dom)渲染UI。完成UI渲染之后,会调用剩余的生命周期函数,所以异常处理也会在这部分进行

分配优先级

其上所列出的fiber结构中有个expirationTime。

expirationTime本质上是fiber work执行的优先级。

// 源码中的priorityLevel优先级划分
export const NoWork = 0;
// 仅仅比Never高一点 为了保证连续必须完整完成
export const Never = 1;
export const Idle = 2;
export const Sync = MAX_SIGNED_31_BIT_INT;//整型最大数值,是V8中针对32位系统所设置的最大值
export const Batched = Sync - 1;

源码中的computeExpirationForFiber函数,该方法用于计算fiber更新任务的最晚执行时间,进行比较后,决定是否继续做下一个任务。



//为fiber对象计算expirationTime
function computeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber) {
  ...
  // 根据调度优先级计算ExpirationTime
    const priorityLevel = getCurrentPriorityLevel();
    switch (priorityLevel) {
      case ImmediatePriority:
        expirationTime = Sync;
        break;
        //高优先级 如由用户输入设计交互的任务
      case UserBlockingPriority:
        expirationTime = computeInteractiveExpiration(currentTime);
        break;
        // 正常的异步任务
      case NormalPriority:
        // This is a normal, concurrent update
        expirationTime = computeAsyncExpiration(currentTime);
        break;
      case LowPriority:
      case IdlePriority:
        expirationTime = Never;
        break;
      default:
        invariant(
          false,
          'Unknown priority level. This error is likely caused by a bug in ' +
            'React. Please file an issue.',
        );
    }
    ...
}

export const LOW_PRIORITY_EXPIRATION = 5000
export const LOW_PRIORITY_BATCH_SIZE = 250

export function computeAsyncExpiration(
  currentTime: ExpirationTime,
): ExpirationTime {
  return computeExpirationBucket(
    currentTime,
    LOW_PRIORITY_EXPIRATION,
    LOW_PRIORITY_BATCH_SIZE,
  )
}

export const HIGH_PRIORITY_EXPIRATION = __DEV__ ? 500 : 150
export const HIGH_PRIORITY_BATCH_SIZE = 100

export function computeInteractiveExpiration(currentTime: ExpirationTime) {
  return computeExpirationBucket(
    currentTime,
    HIGH_PRIORITY_EXPIRATION,
    HIGH_PRIORITY_BATCH_SIZE,
  )
}

function computeExpirationBucket(
  currentTime,
  expirationInMs,
  bucketSizeMs,
): ExpirationTime {
  return (
    MAGIC_NUMBER_OFFSET -
    ceiling(
    // 之前的算法
     //currentTime - MAGIC_NUMBER_OFFSET + expirationInMs / UNIT_SIZE,
      MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE,
      bucketSizeMs / UNIT_SIZE,
    )
  );
}
// 我们把公式整理一下:
// low
 1073741821-ceiling(1073741821-currentTime+500,25) =>
 1073741796-((1073742321-currentTime)/25 | 0)*25
// high 
1073741821-ceiling(1073741821-currentTime+15,10)

简单来说,最终结果是以25为单位向上增加的,比如说我们输入102 - 126之间,最终得到的结果都是625,但是到了127得到的结果就是650了,这就是除以25取整的效果。

即计算出的React低优先级update的expirationTime间隔是25ms, React让两个相近(25ms内)的update得到相同的expirationTime,目的就是让这两个update自动合并成一个Update,从而达到批量更新的目的。就像提到的doubleBuffer一样,React为提高性能,考虑得非常全面!

expiration算法源码

推荐阅读:jokcy大神解析=》expirationTime计算

执行优先级

那么Fiber是如何做到异步实现不同优先级任务的协调执行的

这里要介绍介绍浏览器提供的两个API:requestIdleCallback和requestAnimationFrame:

requestIdleCallback:
在浏览器空闲时段内调用的函数排队。是开发人员可以在主事件循环上执行后台和低优先级工作而不会影响延迟关键事件,如动画和输入响应。

其在回调参数中IdleDeadline可以获取到当前帧剩余的时间。利用这个信息可以合理的安排当前帧需要做的事情,如果时间足够,那继续做下一个任务,如果时间不够就歇一歇。

requestAnimationFrame:告诉浏览器你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画

合作式调度:这是一种’契约‘调度,要求我们的程序和浏览器紧密结合,互相信任。比如可以由浏览器给我们分配执行时间片,我们要按照约定在这个时间内执行完毕,并将控制权还给浏览器。


Fiber所做的就是需要分解渲染任务,然后根据优先级使用API调度,异步执行指定任务:

  • 低优先级任务由requestIdleCallback处理,限制任务执行时间,以切分任务,同时避免任务长时间执行,阻塞UI渲染而导致掉帧。
  • 高优先级任务,如动画相关的由requestAnimationFrame处理;

并不是所有的浏览器都支持requestIdleCallback,但是React内部实现了自己的polyfill,所以不必担心浏览器兼容性问题。polyfill实现主要是通过rAF+postmessage实现的(最新版本去掉了rAF,有兴趣的童鞋可以看看=》SchedulerHostConfig

生命周期

因为其在协调阶段任务可被打断的特点,任务在切片后运行完一段便将控制权交还到react负责任务调度的模块,再根据任务的优先级,继续运行后面的任务。所以会导致某些组件渲染到一半便会打断以运行其他紧急,优先级更高的任务,运行完却不会继续之前中断的部分,而是重新开始,所以在协调的所有生命周期都会面临这种被多次调用的情况。
为了限制这种被多次重复调用,耗费性能的情况出现,react官方一步步把处在协调阶段的部分生命周期进行移除。

废弃:

  • componentWillMount
  • componentWillUpdate
  • componentWillReceiveProps

新增:

newLifeCircle

为什么新的生命周期用static

static 是ES6的写法,当我们定义一个函数为static时,就意味着无法通过this调用我们在类中定义的方法

通过static的写法和函数参数,可以感觉React在和我说:请只根据newProps来设定derived state,不要通过this这些东西来调用帮助方法,可能会越帮越乱。用专业术语说:getDerivedStateFromProps应该是个纯函数,没有副作用(side effect)。

getDerivedStateFromError和componentDidCatch之间的区别是什么?

简而言之,因为所处阶段的不同而功能不同。

getDerivedStateFromError是在reconciliation阶段触发,所以getDerivedStateFromError进行捕获错误后进行组件的状态变更,不允许出现副作用。

static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染可以显降级 UI
    return { hasError: true };
}

componentDidCatch因为在commit阶段,因此允许执行副作用。 它应该用于记录错误之类的情况:

componentDidCatch(error, info) {
    // "组件堆栈" 例子:
    //   in ComponentThatThrows (created by App)
    //   in ErrorBoundary (created by App)
    //   in div (created by App)
    //   in App
    logComponentStackToMyService(info.componentStack);
  }

生命周期相关资料点这里=》生命周期

Suspense

Suspense的实现很诡异,也备受争议。
用Dan的原话讲:你将会恨死它,然后你会爱上他。

Suspense功能想解决从react出生到现在都存在的「异步副作用」的问题,而且解决得非常的优雅,使用的是「异步但是同步的写法」.

Suspense暂时只是用于搭配lazy进行代码分割,在组件等待某事时“暂停”渲染的能力,并显示加载的loading,但他的作用远远不止如此,当下在concurrent mode实验阶段文档下提供了一种suspense处理异步请求获取数据的方法。

用法

// 懒加载组件切换时显示过渡组件
const ProfilePage = React.lazy(() => import('./ProfilePage')); // Lazy-loaded

// Show a spinner while the profile is loading
<Suspense fallback={<Spinner />}>
  <ProfilePage />
</Suspense>
// 异步获取数据
import { unstable_createResource } from 'react-cache'

const resource = unstable_createResource((id) => {
  return fetch(`/demo/${id}`)
})

function ProfilePage() {
  return (
    <Suspense fallback={<h1>Loading profile...</h1>}>
      <ProfileDetails />
      <Suspense fallback={<h1>Loading posts...</h1>}>
        <ProfileTimeline />
      </Suspense>
    </Suspense>
  );
}

function ProfileDetails() {
  // Try to read user info, although it might not have loaded yet
  const user = resource.user.read();
  return <h1>{user.name}</h1>;
}

function ProfileTimeline() {
  // Try to read posts, although they might not have loaded yet
  const posts = resource.posts.read();
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.text}</li>
      ))}
    </ul>
  );
}
  • 在render函数中,我们可以写入一个异步请求,请求数据
  • react会从我们缓存中读取这个缓存
  • 如果有缓存了,直接进行正常的render
  • 如果没有缓存,那么会抛出一个异常,这个异常是一个promise
  • 当这个promise完成后(请求数据完成),react会继续回到原来的render中(实际上是重新执行一遍render),把数据render出来
  • 完全同步写法,没有任何异步callback之类的东西

如果你还没有明白这是什么意思那我简单的表述成下面这句话:

调用render函数->发现有异步请求->悬停,等待异步请求结果->再渲染展示数据

看着是非常神奇的,用同步方法写异步,而且没有yield/async/await,简直能把人看傻眼了。这么做的好处自然就是,我们的思维逻辑非常的简单,清楚,没有callback,没有其他任何玩意,不能不说,看似优雅了非常多而且牛逼。

官方文档指出它还将提供官方的方法进行数据获取

原理

看一下react提供的unstable_createResource源码

export function unstable_createResource(fetch, maybeHashInput) {
  const resource = {
    read(input) {
      ...
      const result = accessResult(resource, fetch, input, key);
      switch (result.status) {
        // 还未完成直接抛出自身promise
        case Pending: {
          const suspender = result.value;
          throw suspender;
        }
        case Resolved: {
          const value = result.value;
          return value;
        }
        case Rejected: {
          const error = result.value;
          throw error;
        }
        default:
          // Should be unreachable
          return (undefined: any);
      }
    },
  };
  return resource;
}

为此,React使用Promises。
组件可以在其render方法(或在组件的渲染过程中调用的任何东西,例如新的静态getDerivedStateFromProps)中抛出Promise。
React捕获了抛出的Promise,并在树上寻找最接近的Suspense组件,Suspense其本身具有componentDidCatch,将promise当成error捕获,等待其执行完成其更改状态重新渲染子组件。

Suspense组件将一个元素(fallback 作为其后备道具,无论子节点在何处或为什么挂起,都会在其子树被挂起时进行渲染。

如何达成异常捕获

  1. reconciliation阶段的 renderRoot 函数,对应异常处理方法是 throwException
  2. commit阶段的 commitRoot 函数,对应异常处理方法是 dispatch

reconciliation阶段的异常捕获

react-reconciler中的performConcurrentWorkOnRoot

// This is the entry point for every concurrent task, i.e. anything that
// goes through Scheduler.
// 这里是每一个通过Scheduler的concurrent任务的入口
function performConcurrentWorkOnRoot(root, didTimeout) {
    ...
    do {
        try {
            //开始执行Concurrent任务直到Scheduler要求我们让步
            workLoopConcurrent();
            break;
        } catch (thrownValue) {
            handleError(root, thrownValue);
        }
    } while (true);
    ...
}

function handleError(root, thrownValue) {
    ...
      throwException(
        root,
        workInProgress.return,
        workInProgress,
        thrownValue,
        renderExpirationTime,
      );
      workInProgress = completeUnitOfWork(workInProgress);
   ...
}

throwException

do {
    switch (workInProgress.tag) {
      ....
      case ClassComponent:
        // Capture and retry
        const errorInfo = value;
        const ctor = workInProgress.type;
        const instance = workInProgress.stateNode;
        if (
          (workInProgress.effectTag & DidCapture) === NoEffect &&
          (typeof ctor.getDerivedStateFromError === 'function' ||
            (instance !== null &&
              typeof instance.componentDidCatch === 'function' &&
              !isAlreadyFailedLegacyErrorBoundary(instance)))
        ) {
          workInProgress.effectTag |= ShouldCapture;
          workInProgress.expirationTime = renderExpirationTime;
          // Schedule the error boundary to re-render using updated state
          const update = createClassErrorUpdate(
            workInProgress,
            errorInfo,
            renderExpirationTime,
          );
          enqueueCapturedUpdate(workInProgress, update);
          return;
        }
    }
    ...
}
    

throwException函数分为两部分
1、遍历当前异常节点的所有父节点,找到对应的错误信息(错误名称、调用栈等),这部分代码在上面中没有展示出来

2、第二部分是遍历当前异常节点的所有父节点,判断各节点的类型,主要还是上面提到的两种类型,这里重点讲ClassComponent类型,判断该节点是否是异常边界组件(通过判断是否存在componentDidCatch生命周期函数等),如果是找到异常边界组件,则调用 createClassErrorUpdate函数新建update,并将此update放入此节点的异常更新队列中,在后续更新中,会更新此队列中的更新工作

commit阶段

ReactFiberWorkLoop中的finishConcurrentRender=》
commitRoot=》
commitRootImpl=》captureCommitPhaseError

commit被分为几个子阶段,每个阶段都try catch调用了一次captureCommitPhaseError

  1. 突变(mutate)前阶段:我们在突变前先读出主树的状态,getSnapshotBeforeUpdate在这里被调用
  2. 突变阶段:我们在这个阶段更改主树,完成WIP树转变为current树
  3. 样式阶段:调用从被更改后主树读取的effect
export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) {
  if (sourceFiber.tag === HostRoot) {
    // Error was thrown at the root. There is no parent, so the root
    // itself should capture it.
    captureCommitPhaseErrorOnRoot(sourceFiber, sourceFiber, error);
    return;
  }

  let fiber = sourceFiber.return;
  while (fiber !== null) {
    if (fiber.tag === HostRoot) {
      captureCommitPhaseErrorOnRoot(fiber, sourceFiber, error);
      return;
    } else if (fiber.tag === ClassComponent) {
      const ctor = fiber.type;
      const instance = fiber.stateNode;
      if (
        typeof ctor.getDerivedStateFromError === 'function' ||
        (typeof instance.componentDidCatch === 'function' &&
          !isAlreadyFailedLegacyErrorBoundary(instance))
      ) {
        const errorInfo = createCapturedValue(error, sourceFiber);
        const update = createClassErrorUpdate(
          fiber,
          errorInfo,
          // TODO: This is always sync
          Sync,
        );
        enqueueUpdate(fiber, update);
        const root = markUpdateTimeFromFiberToRoot(fiber, Sync);
        if (root !== null) {
          ensureRootIsScheduled(root);
          schedulePendingInteractions(root, Sync);
        }
        return;
      }
    }
    fiber = fiber.return;
  }
}

captureCommitPhaseError函数做的事情和上部分的 throwException 类似,遍历当前异常节点的所有父节点,找到异常边界组件(有componentDidCatch生命周期函数的组件),新建update,在update.callback中调用组件的componentDidCatch生命周期函数。

细心的小伙伴应该注意到,throwException 和 captureCommitPhaseError在遍历节点时,是从异常节点的父节点开始遍历,所以异常捕获一般由拥有componentDidCatch或getDerivedStateFromError的异常边界组件进行包裹,而其是无法捕获并处理自身的报错。

Hook相关

Function Component和Class Component

Class component 劣势

  1. 状态逻辑难复用:在组件之间复用状态逻辑很难,可能要用到 render props (渲染属性)或者 HOC(高阶组件),但无论是渲染属性,还是高阶组件,都会在原先的组件外包裹一层父容器(一般都是 div 元素),导致层级冗余 趋向复杂难以维护:
  2. 在生命周期函数中混杂不相干的逻辑(如:在 componentDidMount 中注册事件以及其他的逻辑,在 componentWillUnmount 中卸载事件,这样分散不集中的写法,很容易写出 bug ) 类组件中到处都是对状态的访问和处理,导致组件难以拆分成更小的组件
  3. this 指向问题:父组件给子组件传递函数时,必须绑定 this

但是在16.8之前react的函数式组件十分羸弱,基本只能作用于纯展示组件,主要因为缺少state和生命周期。

hooks优势

  • 能优化类组件的三大问题
  • 能在无需修改组件结构的情况下复用状态逻辑(自定义 Hooks )
  • 能将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据)
  • 副作用的关注点分离:副作用指那些没有发生在数据向视图转换过程中的逻辑,如 ajax 请求、访问原生dom 元素、本地持久化缓存、绑定/解绑事件、添加订阅、设置定时器、记录日志等。以往这些副作用都是写在类组件生命周期函数中的。而 useEffect 在全部渲染完毕后才会执行,useLayoutEffect 会在浏览器 layout 之后,painting 之前执行。

capture props和capture value特性

capture props

class ProfilePage extends React.Component {
  showMessage = () => {
    alert("Followed " + this.props.user);
  };

  handleClick = () => {
    setTimeout(this.showMessage, 3000);
  };

  render() {
    return <button onClick={this.handleClick}>Follow</button>;
  }
}
function ProfilePage(props) {
  const showMessage = () => {
    alert("Followed " + props.user);
  };

  const handleClick = () => {
    setTimeout(showMessage, 3000);
  };

  return <button onClick={handleClick}>Follow</button>;
}

这两个组件都描述了同一个逻辑:点击按钮 3 秒后 alert 父级传入的用户名。

那么 React 文档中描述的 props 不是不可变(Immutable) 数据吗?为啥在运行时还会发生变化呢?

原因在于,虽然 props 不可变,是 this 在 Class Component 中是可变的,因此 this.props 的调用会导致每次都访问最新的 props。

无可厚非,为了在生命周期和render重能拿到最新的版本react本身会实时更改this,这是this在class组件的本职。

这揭露了关于用户界面的有趣观察,如果我们说ui从概念上是一个当前应用状态的函数,事件处理就是render结果的一部分,我们的事件处理属于拥有特定props或state的render。每次 Render 的内容都会形成一个快照并保留下来,因此当状态变更而 Rerender 时,就形成了 N 个 Render 状态,而每个 Render 状态都拥有自己固定不变的 Props 与 State。

然而在setTimeout的回调中获取this.props会打断这种的关联,失去了与某一特定render绑定,所以也失去了正确的props。

而 Function Component 不存在 this.props 的语法,因此 props 总是不可变的。

测试地址

hook中的capture value

function MessageThread() {
  const [message, setMessage] = useState("");

  const showMessage = () => {
    alert("You said: " + message);
  };

  const handleSendClick = () => {
    setTimeout(showMessage, 3000);
  };

  const handleMessageChange = e => {
    setMessage(e.target.value);
  };

  return (
    <>
      <input value={message} onChange={handleMessageChange} />
      <button onClick={handleSendClick}>Send</button>
    </>
  );
}

hook重同样有capture value,每次渲染都有自己的 Props and State,如果要时刻获取最新的值,规避 capture value 特性,可以用useRef

const lastest = useRef("");

const showMessage = () => {
    alert("You said: " + lastest.current);
};

const handleSendClick = () => {
    setTimeout(showMessage, 3000);
};

const handleMessageChange = e => {
    lastest.current = e.target.value;
};

测试地址

Hooks实现原理

在上面fiber结构分析可以看出现在的Class component的state和props是记录在fiber上的,在fiber更新后才会更新到component的this.state和props里面,而并不是class component自己调理的过程。这也给了实现hooks的方便,因为hooks是放在function component里面的,他没有自己的this,但我们本身记录state和props就不是放在class component this上面,而是在fiber上面,所以我们有能力记录状态之后,也有能力让function component更新过程当中拿到更新之后的state。

React 依赖于 Hook 的调用顺序

日常调用三次

function Form() {
  const [hero, setHero] = useState('iron man');
  if(hero){
    const [surHero, setSurHero] = useState('Captain America');
  }
  const [nbHero, setNbHero] = useState('hulk');
  // ...
}

来看看我们的useState是怎么实现的

// useState 源码中的链表实现
import React from 'react';
import ReactDOM from 'react-dom';

let firstWorkInProgressHook = {memoizedState: null, next: null};
let workInProgressHook;

function useState(initState) {
    let currentHook = workInProgressHook.next ? workInProgressHook.next : {memoizedState: initState, next: null};

    function setState(newState) {
        currentHook.memoizedState = newState;
        render();
    }
	
	// 假如某个 useState 没有执行,会导致Next指针移动出错,数据存取出错
    if (workInProgressHook.next) {
        // 这里只有组件刷新的时候,才会进入
        // 根据书写顺序来取对应的值
        // console.log(workInProgressHook);
        workInProgressHook = workInProgressHook.next;
    } else {
        // 只有在组件初始化加载时,才会进入
        // 根据书写顺序,存储对应的数据
        // 将 firstWorkInProgressHook 变成一个链表结构
        workInProgressHook.next = currentHook;
        // 将 workInProgressHook 指向 {memoizedState: initState, next: null}
        workInProgressHook = currentHook;
        // console.log(firstWorkInProgressHook);
    }
    return [currentHook.memoizedState, setState];
}

function Counter() {
    // 每次组件重新渲染的时候,这里的 useState 都会重新执行
    const [name, setName] = useState('计数器');
    const [number, setNumber] = useState(0);
    return (
        <>
            <p>{name}:{number}</p>
            <button onClick={() => setName('新计数器' + Date.now())}>新计数器</button>
            <button onClick={() => setNumber(number + 1)}>+</button>
        </>
    )
}

function render() {
    // 每次重新渲染的时候,都将 workInProgressHook 指向 firstWorkInProgressHook
    workInProgressHook = firstWorkInProgressHook;
    ReactDOM.render(<Counter/>, document.getElementById('root'));
}

render();

我们来还原一下这个过程
大家看完应该了解,当下设置currentHook其实是上个workInProgressHook通过next指针进行绑定获取的,所以如果在条件语句中打破了调用顺序,将会导致next指针指向出现偏差,这个时候你传进去的setState是无法正确改变对应的值,因为

各种自定义封装的hooks =》react-use

为什么顺序调用对 React Hooks 很重要?

THE END

第二次在掘金上发文,小陈也是react小菜🐔,希望能跟大家一起讨论学习,向高级前端架构进阶!让我们一起爱上fiber

参考:

如何以及为什么React Fiber使用链表遍历组件树
React Fiber架构
React 源码解析 - reactScheduler 异步任务调度
展望 React 17,回顾 React 往事 全面 深入
这可能是最通俗的 React Fiber(时间分片) 打开方式=>调度策略
全面了解 React 新功能: Suspense 和 Hooks 生命周期
详谈 React Fiber 架构(1)

20200427

Observer api

IntersectionObserver

提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法。祖先元素与视窗(viewport)被称为根(root)。

// demo 达成lazyload
var intersectionObserver = new IntersectionObserver(function(entries) {
  // If intersectionRatio is 0, the target is out of view
  // and we do not need to do anything.
  if (entries[0].intersectionRatio <= 0) return;

  loadItems(10);
  console.log('Loaded new items');
});
// start observing
intersectionObserver.observe(document.querySelector('.scrollerFooter'));

工具=》https://codepen.io/michellebarker/full/xxwLpRG(辅助

Proxy和Reflect

组合达成校验器

function proxyValidator(target: any, validator: any) {
  return new Proxy(target, {
    set: function (target, propKey, value, receiver) {
      if (!!validator[propKey](value)) {
        console.log('=====right');
        return Reflect.set(target, propKey, value, receiver);
      } else {
        throw Error(`值校验错误${String(propKey)}:${value}`);
      }
    },
  });
}

// 用法
const personObj = {
  name: '123',
};

const validator = {
  name: (v: any) => {
    return typeof v === 'string';
  },
};
const formdata = proxyValidator(personObj, validator);
formdata.name = '123';

reflect

reflect
把object一些明显属于语言内部的方法的方法移植到reflect(比如Object.defineProperty)

// 老写法
'assign' in Object // true
// 新写法
Reflect.has(Object, 'assign') // true

CustomEvent 自定义事件

// 添加一个适当的事件监听器
dom1.addEventListener("boom", function(e) { something(e.detail.num) })

// 创建并分发事件
var event = new CustomEvent("boom", {"detail":{"num":10}})
dom1.dispatchEvent(event)

我们来看看CustomEvent的参数介绍:

type 事件的类型名称,如上面代码中的'boom'
CustomEventInit 提供了事件的配置信息,具体有以下几个属性

bubbles 一个布尔值,表明该事件是否会冒泡
cancelable 一个布尔值,表明该事件是否可以被取消
detail 当事件初始化时传递的数据

我们可以通过dispatchEvent来触发自定义事件.其实他的用途有很多,比如创建观察者模式, 实现数据双向绑定, 亦或者在游戏开发中实现打怪掉血,

Typescript + Throttle + Hook

关于typescript的定时器setInterval()坑

// 定时器设置类型

function throttle (func: () => void, delay: number) {
  let timer: NodeJS.Timer | null = null
  return (...args : any[]) => {
    if(timer){
      timer = setTimeout(()=>{
        func.apply(this, args)
        clearTimeout(Number(timer))
        timer = null
      }, delay)
    }
  }
}

let timer: NodeJS.Timer | null = null // 设置
clearTimeout(Number(timer)) // 清除

react-use useThrottle实现

EventSource (Server-sent events的h5接口

EventSource 是服务器推送的一个网络事件接口。一个EventSource实例会对HTTP服务开启一个持久化的连接,以text/event-stream 格式发送事件, 会一直保持开启直到被要求关闭。

一旦连接开启,来自服务端传入的消息会以事件的形式分发至你代码中。如果接收消息中有一个事件字段,触发的事件与事件字段的值相同。如果没有事件字段存在,则将触发通用事件。

与 WebSockets,不同的是,服务端推送是单向的。数据信息被单向从服务端到客户端分发. 当不需要以消息形式将数据从客户端发送到服务器时,这使它们成为绝佳的选择。例如,对于处理社交媒体状态更新,新闻提要或将数据传递到客户端存储机制(如IndexedDB或Web存储)之类的,EventSource无疑是一个有效方案。

front-end

var evtSource = new EventSource('sse.php');
var eventList = document.querySelector('ul');

evtSource.onmessage = function(e) {
  var newElement = document.createElement("li");

  newElement.textContent = "message: " + e.data;
  eventList.appendChild(newElement);
}

node

  const sse = new EventSource('/api/v1/sse');

  /* This will listen only for events 
   * similar to the following:
   * 
   * event: notice
   * data: useful data
   * id: someid
   *
   */
  sse.addEventListener("notice", function(e) { 
    console.log(e.data)
  })

  /* Similarly, this will listen for events 
   * with the field `event: update`
   */
  sse.addEventListener("update", function(e) {
    console.log(e.data)
  })

  /* The event "message" is a special case, as it
   * will capture events without an event field
   * as well as events that have the specific type
   * `event: message` It will not trigger on any
   * other event type.
   */
  sse.addEventListener("message", function(e) {
    console.log(e.data)
  })

压缩图片方案解析

// 生成 input标签
var input = document.createElement("input");
input.type = "file";
input.id = "upload";
input.accept = "image/jpeg,image/png,image/bmp,image/gif";


// 压缩第一步
input.addEventListener("change", (e) => {
  //获取文件
  const file = e.target.files[0];
  // 生成FileReader实例
  const reader = new FileReader();
  // 将 File 对象通过 FileReader 的 readAsDataURL 方法转换为URL格式的字符串(base64编码)。
  reader.readAsDataURL(file);
  reader.onload = (theFile) => {
    // 生成图片类
    const image = new Image();
    // 设置图片标签src属性
    image.src = theFile.target.result;
    image.onload = () => {
        const fileForUpload = convertBase64toFile(processImg(image));
        // 最终压缩完成的file图片
        console.log(fileForUpload,'====')
    };
  };
});


// 第二步

const processImg = (img, quality = 0.7, wh = 1000) => {
  if (!img) {
    return;
  }

  // 生成canvas
  const canvas = document.createElement("canvas");
  // 返回一个用于在画布上绘图的环境。
  const ctx = canvas.getContext("2d");

  let height = img.height;
  let width = img.width;
  let imgWH = 1000;

  if (wh && wh < 1000 && wh > 0) {
    imgWH = wh;
  }

  // 按比例缩小图片
  if (width > imgWH || height > imgWH) {
    const ratio = Math.floor((height / width) * 10) / 10;
    if (width > height) {
      width = imgWH;
      height = imgWH * ratio;
    } else {
      height = imgWH;
      width = height / ratio;
    }
    img.width = width;
    img.height = height;
  }

  canvas.width = width;
  canvas.height = height;

  ctx.fillStyle = "#fff";
  ctx.fillRect(0, 0, width, height);
  // // 在画布上绘制图像
  ctx.drawImage(img, 0, 0, width, height);
  // 调用 canvas 的 toDataURL 方法可以输出 base64 格式的图片。
  return canvas.toDataURL("image/jpeg", quality);
};

// 第三步
const convertBase64toFile = (base64) => {
  const date = new Date().valueOf();
  const imageName = date + ".jpg";
  const imageBlob = dataURItoBlob(base64);

  // 第四步 输出file传给后台上传
  const imageFile = new File([imageBlob], imageName, { type: "image/jpg" });
  return imageFile;
};


// 仍是第三步哈哈哈 将base64的uri专为blob文件格式
// 这段我也不是很懂 设计比较底层的转码
const dataURItoBlob = (dataURI) => {
  // window.atob() 方法用于解码使用 base-64 编码的字符串。
  const byteString = atob(dataURI.split(",")[1]);
  const mimeString = dataURI.split(",")[0].split(":")[1].split(";")[0];
  const arrayBuffer = new ArrayBuffer(byteString.length);
  const int8Array = new Uint8Array(arrayBuffer);
  for (let i = 0; i < byteString.length; i++) {
    int8Array[i] = byteString.charCodeAt(i);
  }
  const blob = new Blob([int8Array], { type: "image/jpg" });
  return blob;
};


blob

Blob 对象表示一个不可变、原始数据的类文件对象。Blob 表示的不一定是JavaScript原生格式的数据。File 接口基于Blob,继承了 blob 的功能并将其扩展使其支持用户系统上的文件。

Blob(blobParts[, options])

File

new File( Array parts, String filename, BlobPropertyBag properties);

20210428

promise弊端

  1. 延时问题(涉及到evnet loop)(http://www.ruanyifeng.com/blog/2014/10/event-loop.html))

  2. promise一旦创建,无法取消

  3. pending状态的时候,无法得知进展到哪一步(比如接口超时,可以借助race方法)

  4. promise会吞掉内部抛出的错误,不会反映到外部。如果最后一个then方法里出现错误,无法发现。(可以采取hack形式,在promise构造函数中判断onRejectedCb的数组长度,如果为0,就是没有注册回调,这个时候就抛出错误,某些库实现done方法,它不会返回一个promise对象,且在done()中未经处理的异常不会被promise实例所捕获)

  5. then方法每次调用都会创建一个新的promise对象,一定程度上造成了内存的浪费

Object.is

解决问题

+0 === -0 // true
NaN === NaN // false

面试总结

按时间顺序

腾讯

  • 缓存
  • url输入发生了啥
  • rn架构
  • typescript (extend 实现 class declare type interface

一面聊了快两个小时,聊了很多基础问题,基本都回答上来了,感觉自己确实公司一面基本都没问题,知识广度已经达到了,但是缺乏一些核心开发技能的深度,以上两个较有深度的问题没有答不上来我滴乖乖,导致二面挂了。

平安惠普

一面二面基本没有难的问题,都回答上来了,但js继承有点问题,二面面试官感觉想让我通过的,结果无缘由挂了,感觉被走流程绩效了?反正莫名其妙。问题缺乏深度,面试体验较差。

极光

拿到offer。问得广度较大,唯一一个问了链表的算法题,如果获取链表倒数第n个对应的值,无脑回答了反转哈哈哈,后来思考了回答双指针,快指针先与慢指针走n步,再开始一起,当快指针到达底部,慢指针当前的值就是要的值,一面聊了很多其他的,typescript和rn架构回答也不是很满意,三面架构面问了cdn的整个流程,因为自己了解的比较透彻,没啥大致的问题。

明源云

一面问的比较浅,问的webpack优化,点明我的认识和实践不够,问了优化方案,这个因为弹药储备很足够,所以面试官说很多自己没想到的点都有回答上来,还挺开心。二面主要交流react fiber架构,正常聊天还挺简单的,三面一开始问了很多前公司喝职业发展的问题,比较深入的聊了http,refer等网络协议的问题,感觉是来压工资的。最后肯定了我的知识面挺广的。拿到offer,但是给的定级t2.2还是2.5,薪资在我的底线,比较抠门 = =,本以为大公司不至于这么少,害。

斗鱼直播

问的不太记得,应该是比较常规的,一个笔试题挺有意思,贴在github上了。
笔试题(注重性能=>#35

货拉拉

  1. 事件机制(包括react中事件的冒泡和捕获 如何组织 问得很详细
  2. css自适应布局
  3. webpack优化的方案
  4. react优化方案
  5. 组件封装的一些思路

不太清楚过不过,不过面试过程很有意思,面试官倾向于了解面试者的思考过程,会提供一些比较特别的开发场景,我回答的不是很满意,尽量回答了一些自己比较了解的,但总归只是刚好达到及格线。

cvte

  • url输入的时候发生了啥(我极其详细了
  • http各个版本介绍 https 中间人攻击(基本会,不够深入 如何进行再对称加密过程攻击。勉强及格吧
  • http2的多路复用(70%
  • 优化过程 首屏优化过程 (也就还可以 说了减少render次数的方案 ssr next 缓存
  • webpack打包过程 (没问题
  • 强缓存协商缓存如何配置?(没搞过 说了原理
  • html解析中script标签和link标签(基本没啥问题
  • 比较有成就感和难度的项目点(这个说了rn动画优化
  • csrf和xss csrf的过程 (60%
  • react为什么生命周期要加上unsafe。(简单
  • 子元素的key(简单

笔试题=》https://codesandbox.io/s/gallant-dust-e0l0o?file=/index.html
相比都较为简单,但会有一些小坑。

组件封装思路

hooks中传入传出api的设计
比如我们rn app需要引入录音功能,确认需要引入一个react-native-sound库,
根据这个库的pai 我们自己设计一个useSound的hook,因为他本身没有提供对应的hooks
需要有什么功能,点击play方法,pause方法,录音的duration属性,播放时长做一个进度条,
我们需要考虑传入传出,传入先传一个考虑录音的url,
尽量少的回调函数的传入,比如我想在报错的时候进行提示,这种我们是不会在hooks里面直接提示的,但按以前我开发的思路,可能会传一个处理error的回调函数进去,会想到再useCallback一下,嗯,觉得很完美,但是其实hooks只是来帮你管理状态的,越多的回调的传入反而让hook显示臃肿,更优化的方法是先用一个useState新增一个error,来捕获存储代码过程中的报错,最后将之抛出。

  1. error callback 回调作为参数传入
  2. 在对应调用方法如play方法中传入错误回调作为参数执行,将error传入
  3. play方法中return一个promise

这里获取error的设计,我是在自定义的play方法中接受第二个参数,进行return err,这样子

异步加载 js 脚本的方法有哪些?

  1. 动态创建script标签,可通过script的onreadyState监视加载。
  2. html5新增的async属性:可跟其他内容并行下载,限制ie9以上,只能加载外部js脚本。
  3. html4的defer,作用与async相同,兼容更好一点,但async只要加载完可立即执行,defer需等待在dom加载完毕后执行,在window.onload之前,其他没有添加defer属性的script标签之后。
  4. 利用XHR异步加载js内容并执行。
  5. iframe方式。

20200522

腾讯面试

  • react native跟原生是怎么交互的
  • cookie有什么可以作为唯一标识
  • 三次握手和挥手要更清晰

csp(内容安全策略

内容安全策略(CSP)是一个 HTTP Header,CSP 通过告诉浏览器一系列规则,严格规定页面中哪些资源允许有哪些来源, 不在指定范围内的统统拒绝。

使用它是防止跨站点脚本(XSS)漏洞的最佳方法。由于难以使用 CSP 对现有网站进行改造(可通过渐进式的方法),因此 CSP 对于所有新网站都是强制性的,强烈建议对所有现有高风险站点进行 CSP 策略配置。

CSP被设计出来的目的就是为了效防范内容注入攻击,如XSS攻击等.

它通过让开发者对自己WEB应用声明一个外部资源加载的白名单,使得客户端在运行WEB应用时对外部资源的加载做出筛选和识别,只加载被允许的网站资源.对于不被允许的网站资源不予加载和执行.同时,还可以将WEB应用中出现的不被允许的资源链接和详情报告给我们指定的网址.如此,大大增强了WEB应用的安全性.使得攻击者即使发现了漏洞,也没法注入脚本,除非还控制了一台列入了白名单的服务器.

使用方式

  1. meta标签
<meta http-equiv="Content-Security-Policy" content="default-src 'self';">
<meta http-equiv="content-security-policy-report-only" content="default-src 'self';">
  1. http header

一种是后端开发或服务运维人员对页面的HTTP请求的响应配置Content-Security-Policy属性,如在NGINX服务器上配置如下

OAuth2

OAuth(Open Authorization,开放授权)是为用户资源的授权定义了一个安全、开放及简单的标准,第三方无需知道用户的账号及密码,就可获取到用户的授权信息

OAuth2.0是OAuth协议的延续版本,但不向后兼容OAuth 1.0即完全废止了OAuth1.0

  1. 第三方应用向资源持有者请求获取资源
  2. 资源持有者授权给予第三方应用一个许可
  3. 第三方应用将该许可给予认证服务器进行认证,如果认证成功,返回一个Access Token
  4. 第三方应用使用该access token到资源服务器处获取该access token对应的资源(也就是第一步中资源持有者自身的资源)

腾讯面试题

const arr = [101,19,12,51,32,7,103,8];

问题一: 找出连续最大升序的数量

示例 1:
输入: [1,3,5,4,7]
输出: 3
解释: 最长连续递增序列是 [1,3,5], 长度为3。
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为5和7在原数组里被4隔开。

问题二: 找出不连续最大升序的数量

nextJs服务端渲染

React SSR 详解【近 1W 字】+ 2个项目实战
SSR 之所以能够实现,本质上是因为虚拟 DOM 的存在
React 中同构(SSR)原理脉络梳理

因为服务端返回的 HTML 是字符串,虽然有内容,但是各个组件没有事件,客户端的仓库中也没有数据,可以看做是干瘪的字符串。客户端会根据这些字符串完成 React 的初始化工作,比如创建组件实例、绑定事件、初始化仓库数据等。hydrate 在这个过程中起到了非常重要的作用,俗称“注水”,可以理解为给干瘪的种子注入水分,使其更具生机。

在使用 Next.js 时, 打开浏览器控制台 => 找到 network => 找到当前路由的请求并查看 response => 可以看到服务端返回的 html 里包含着当前页面需要的数据,这样客户端就不会重新发起请求了,靠的就是 ReactDOM.hydrate 。

image

实现一个数组去重的方法

1、ES6中新增了数据类型set,set的一个最大的特点就是数据不重复。Set函数可以接受一个数组(或类数组对象)作为参数来初始化,利用该特性也能做到给数组去重。

return [...new set(array)] // 1
return Array.from(new set(array)) // 2
  1. 循环数组并利用数组的indexOf()方法判断当前的值是否已存在,如果不存在则存进新数组。
let res = []
for (let i = 0; i < arr.length; i++) {
    if (res.indexOf(arr[i]) === -1) {
        res.push(arr[i])
    }
}
return res
  1. 利用对象属性值不重复,给对象添加属性,并用Object.keys()获取。
  2. 利用includes,同2

20200519 欢聚时代面试

双向绑定

https://zhuanlan.zhihu.com/p/45081605

vue2 defindproperty双向绑定与vue3 proxy的区别

vue2 defindproperty的缺陷:

  • 需要遍历对象下的所有属性和子元素的属性,消耗较大
  • 不支持Set,Map,Class和数组等类型
  • defindproperty无法对数组进行劫持
  • 新增或删除的属性无法劫持

代替循环遍历

在使用Object.defineProperty时,我们必须循环遍历所有的域值才能劫持每一个属性,说实在这就是个hack。

Object.keys(data).forEach((prop) => { ... }

而Proxy的劫持手段则是官宣标准——直接监听data的所有域值。

//data is our source object being observed
const observer = new Proxy(data, {
    get(obj, prop) { ... },
    set(obj, prop, newVal) { ... },
    deleteProperty() {
        //invoked when property from source data object is deleted
    }
})

Proxy构造函数的第一个参数是原始数据data;第二个参数是一个叫handler的处理器对象。Handler是一系列的代理方法集合,它的作用是拦截所有发生在data数据上的操作。这里的get()和set()是最常用的两个方法,分别代理访问和赋值两个操作。在Observer里,它们的作用是分别调用dep.depend()和dep.notify()实现订阅和发布。直接反映在Vue里的好处就是:我们不再需要使用Vue.$set()这类响应式操作了。除此之外,handler共有十三种劫持方式,比如deleteProperty就是用于劫持域删除。

监听数组变化

Proxy不需要各种hack技术就可以无压力监听数组变化;甚至有比hack更强大的功能——自动检测length。除此之外,Proxy还有多达13种拦截方式,包括construct、deleteProperty、apply等等操作;而且性能也远优于Object.defineProperty,这应该就是所谓的新标准红利吧。

vue如何在defindproperty中实现对数组的劫持

使用了函数劫持的方式,重写了数组的方法,Vue将data中的数组进行了原型链重写,指向了自己定义的数组原型方法。这样当调用数组api时,可以通知依赖更新。如果数组中包含着引用类型,会对数组中的引用类型再次递归遍历进行监控。这样就实现了监测数组变化。

const arrayProto = Array.prototype//原生Array的原型
export const arrayMethods = Object.create(arrayProto);
[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
].forEach(function (method) {
  const original = arrayProto[method]//缓存元素数组原型
  //这里重写了数组的几个原型方法
  def(arrayMethods, method, function mutator () {
    //这里备份一份参数应该是从性能方面的考虑
    let i = arguments.length
    const args = new Array(i)
    while (i--) {
      args[i] = arguments[i]
    }
    const result = original.apply(this, args)//原始方法求值
    const ob = this.__ob__//这里this.__ob__指向的是数据的Observer
    let inserted
    switch (method) {
      case 'push':
        inserted = args
        break
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})

//定义属性
function def (obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  });
}
// 以上代码 相当于
var bar = [1,2];
bar.__proto__ = arrayMethod;
// 执行
bar.push(3); 就会触发arrayMethods中的代码:ob.dep.notify()就会被触发,就会通知watcher触发template的更新。

webpack构建深入

https://juejin.im/post/5d614dc96fb9a06ae3726b3e#heading-6

缓存、多核、抽离以及拆分

缓存(cache loader

cache loader:将 loader 的编译结果写入硬盘缓存,再次构建如果文件没有发生变化则会直接拉取缓存

多核(happypack

Happypack 的作用就是将文件解析任务分解成多个子进程并发执行。子进程处理完任务后再将结果发送给主进程。所以可以大大提升 Webpack 的项目构件速度

抽离

DllPlugin

DLL(Dynamic Link Library)文件为动态链接库文件,在Windows中,许多应用程序并不是一个完整的可执行文件,它们被分割成一些相对独立的动态链接库,即DLL文件,放置于系统中。当我们执行某一个程序时,相应的DLL文件就会被调用。

为什么要使用Dll

通常来说,我们的代码都可以至少简单区分成业务代码和第三方库。如果不做处理,每次构建时都需要把所有的代码重新构建一次,耗费大量的时间。然后大部分情况下,很多第三方库的代码并不会发生变更(除非是版本升级),这时就可以用到dll:把复用性较高的第三方模块打包到动态链接库中,在不升级这些库的情况下,动态库不需要重新打包,每次构建只重新打包业务代码。

还是上面的例子:把每次构建,当做是生产产品的过程,我们把生产螺丝的过程先提取出来,之后我们不管调整产品的功能或者设计(对应于业务代码变更),都不必重复生产螺丝(第三方模块不需要重复打包);除非是产品要使用新型号的螺丝(第三方模块需要升级),才需要去重新生产新的螺丝,然后接下来又可以专注于调整产品本身。

作用

首先从前面的介绍,至少可以看出dll的两个作用

  1. 分离代码,业务代码和第三方模块可以被打包到不同的文件里,这个有几个好处:
  • 避免打包出单个文件的大小太大,不利于调试
  • 将单个大文件拆成多个小文件之后,一定情况下有利于加载(不超出浏览器一次性请求的文件数情况下,并行下载肯定比串行快)
  1. 提升构建速度。第三方库没有变更时,由于我们只构建业务相关代码,相比全部重新构建自然要快的多。

Externals

将这些不需要打包的静态资源从构建逻辑中剔除出去,而使用 CDN 的方式,去引用它们。

拆分(集群编译)

rem

em:根据父元素的字体大小,父元素字体大小是16px,1em就是16px。

rem:root em,根据根元素=》html的字体大小来判定。

适配方案:根元素字体大小根据屏幕显示宽度来进行变动,切分成10份,当屏幕宽度为750时,一份1rem为75,当屏幕宽度为350时,一份1rem为35,则可以根据相应的设计图大小与元素大小的比例,换算成对应的rem。

const docEl = document.documentElement
const width = docEl.clientWidth
const rem = width / 10 + 'px'
docEl.style.fontSize = rem

animation动画

animation-duration

1px的问题

原因

那么为什么会产生这个问题呢?主要是跟一个东西有关,DPR(devicePixelRatio) 设备像素比,它是默认缩放为100%的情况下,设备像素和CSS像素的比值。

window.devicePixelRatio=物理像素 /CSS像素

复制代码目前主流的屏幕DPR=2 (iPhone 8),或者3 (iPhone 8 Plus)。拿2倍屏来说,设备的物理像素要实现1像素,而DPR=2,所以css 像素只能是 0.5。一般设计稿是按照750来设计的,它上面的1px是以750来参照的,而我们写css样式是以设备375为参照的,所以我们应该写的0.5px就好了啊! 试过了就知道,iOS 8+系统支持,安卓系统不支持。

解决方案

  1. 关于移动端开发1px线的一些理解和解决办法

  2. 7种方法解决移动端Retina屏幕1px边框问题

  3. 使用Flexible实现手淘H5页面的终端适配

WWDC对iOS统给出的方案

在 WWDC大会上,给出来了1px方案,当写 0.5px的时候,就会显示一个物理像素宽度的 border,而不是一个css像素的 border。 所以在iOS下,你可以这样写。

缺点: 无法兼容安卓设备、 iOS 8 以下设备。

viewport + rem 实现

同时通过设置对应viewport的rem基准值,这种方式就可以像以前一样轻松愉快的写1px了。

在devicePixelRatio = 2 时,输出viewport:

<meta name="viewport" content="initial-scale=0.5, maximum-scale=0.5, minimum-scale=0.5, user-scalable=no">

在devicePixelRatio = 3 时,输出viewport:

<meta name="viewport" content="initial-scale=0.3333333333333333, maximum-scale=0.3333333333333333, minimum-scale=0.3333333333333333, user-scalable=no">

伪类 + scale

元素本身不设置border,用设置伪类设置绝对定位+宽度100%+高度1px,然后scale(0.5)让高度缩小一半,新边框就相当于0.5px。

react-native 如何使用 code-push 热更新

https://github.com/lisong/code-push-server/blob/master/docs/react-native-code-push.md

code push

code push 调用 react native 的打包命令,将当前环境的非 native 代码全量打包成一个 bundle 文件,然后上传到微软云服务器(Windows Azure)。 在 app 中启动页(或 splash 页)编写请求更新的代码(请求包含了本地版本,hashCode、appToken 等信息),微软服务端对比本地 js bundle 版本和微软服务器的版本,如果本地版本低,就下载新的 js bundle 下来后实现更新(code push 框架实现)。

https://www.jianshu.com/p/8e08c7661275

RN对性能监控的思考及工具分享

Gulp和Webpack功能实现对比 (我为什么要替换gulp)

基于任务运行

gulp是侧重于前端开发的整个过程的控制管理,通过给gulp配置不同的task来让gulp实现不同的功能,从而构建整个前端开发流程。

打包是根据在某个文件路径下的所有文件类型进行打包

基于模块化打包

webpack更侧重于模块打包,把所有开发资源看作模块。

打包:从入口开始,用loader对文件进行处理解析后获取文件模块之间的依赖关系,通过递归获取整个依赖关系图。

所以,Webpack中对资源文件的处理是通过入口文件产生的依赖形成的,不会像Gulp那样,配置好路径后,该路径下所有规定的文件都会受影响。

模块化就是对内容的管理,是为了解耦合。

特点:

  1. 文件模块化:不管是 CSS、JS、Image 还是 HTML 都可以互相引用,通过定义 entry.js,对所有依赖的文件进行跟踪,将各个模块通过 loader 和 plugins 处理,然后打包在一起。
  2. 按需加载:打包过程中 Webpack 通过 Code Splitting 功能将文件分为多个 chunks,还可以将重复的部分单独提取出来作为 commonChunk,从而实现按需加载。

综上所述,Webpack 特别适合配合 React.js、Vue.js 构建单页面应用以及需要多人合作的大型项目,在规范流程都已约定好的情况下往往能极大的提升开发效率与开发体验。

animation

// animation-name,animation-duration, animation-timing-function,animation-delay,animation-iteration-count,animation-direction,animation-fill-mode 和 animation-play-state

@keyframes spin {
  from {
    transform: rotate(0)
  }
  to {
    transform: rotate(360deg)
  }
}

.App-logo {
  height: 40vmin;
  pointer-events: none;
  animation: spin 4s linear infinite;
}

设计模式

redux中间件

就是对dispatch的增强,通过高阶函数+compose,获取dispatch

// 调用applyMiddleware
applyMiddleware(thunk, logger)

export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    const store = createStore(...args)
    let dispatch = () => {
      throw new Error(
        'Dispatching while constructing your middleware is not allowed. ' +
        'Other middleware would not be applied to this dispatch.'
      )
    }

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => {
        return dispatch(...args)
      }
    }
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)
    return {
      ...store,
      dispatch
    }
  }
}

react native的生命周期

react use-redux 自上而下的数据管理

热更新

20200526

event-loop

  • 每一轮 Event Loop 都会伴随着渲染吗?
  • requestAnimationFrame 在哪个阶段执行,在渲染前还是后?在 microTask 的前还是后?
  • requestIdleCallback 在哪个阶段执行?如何去执行?在渲染前还是后?在 microTask 的前还是后?
  • resize、scroll 这些事件是何时去派发的。

流程

  1. 从任务队列中取出一个宏任务并执行。
  2. 检查微任务队列,执行并清空位人物队列,如果在微任务的执行中又加入了新的微任务,也会在这一步一起执行。
  3. 判断是否需要渲染,不一定每一轮 event loop 都会对应一次浏览 器渲染。
  4. 如果需要渲染,这个时候需要执行一些渲染前的准备工作
  • 如果窗口大小变化监听resize
  • 页面滚动则scroll
  • 执行帧动画回调,也就是 requestAnimationFrame 的回调
  • 对于需要渲染的文档, 执行 IntersectionObserver 的回调。
  1. 重新渲染页面
  2. 判断 task队列和microTask队列是否都为空,如果是的话,则进行 Idle 空闲周期的算法,判断是否要执行 requestIdleCallback 的回调函数。(后文会详解)

IntersectionObserver

一直以来我们要监控2个元素的相对位置,总是比较麻烦的,而且之前也只能通过js以及每个元素的top值来控制,这也极易拖慢整个网站的性能。然而,随着网页的发展,对上述检测的需求也随之增加,多种情况下都需要用到元素交集变化的信息。

Intersection Observer的出现,解决了这个问题,Intersection Observer API 会在浏览器注册一个观察者,并且可以设定据地要观察的目标(target),当目标元素(target)以及根元素或者指定的外层元素(root元素)相互交叉的时候触发事件。

闭包

闭包是啥,如何形成

闭包是有权访问另一个函数作用域中的变量的函数:当函数1向外暴露了可以改变内部局部变量的方法(内部匿名函数,外部函数2接受匿名函数,匿名函数作用域链在初始化时包含了函数1的变量对象,可以调用函数1所有的内部变量。一般函数执行完局部活动对象就会进行销毁,内存仅仅只会保存全局作用域,但函数1在执行完毕其活动对象也不会进行销毁,因为匿名函数的作用域链仍然在对其进行调用。(参考高程

作用

可以读取函数内部的变量,让变量的值始终保持在内存中。栗子:实现函数curry化。

注意

内存泄露。

webpacl vs rollup

webpack

  1. 代码分割 模块依赖关系
  2. loader
  3. 插件系统

Rollup

Rollup是下一代JavaScript模块打包工具。开发者可以在你的应用或库中使用ES2015模块,然后高效地将它们打包成一个单一文件用于浏览器和Node.js使用。 Rollup最令人激动的地方,就是能让打包文件体积很小。这么说很难理解,更详细的解释:相比其他JavaScript打包工具,Rollup总能打出更小,更快的包。因为Rollup基于ES2015模块,比Webpack和Browserify使用的CommonJS模块机制更高效。这也让Rollup从模块中删除无用的代码,即tree-shaking变得更容易。

Tree-shaking

这个特点,是Rollup最初推出时的一大特点。Rollup通过对代码的静态分析,分析出冗余代码,在最终的打包文件中将这些冗余代码删除掉,进一步缩小代码体积。这是目前大部分构建工具所不具备的特点(Webpack 2.0+已经支持了,但是我本人觉得没有Rollup做得干净)。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.