Giter VIP home page Giter VIP logo

blog's Introduction

Hi there 👋

I'm a developer who works in China. and I'm trying to be a full-stack developer.

AwesomeDevin's Github Stats

Most Used Languages

These open-source projects may help you 😆, thanks for your ✨ star ✨:

  • zustand-vue - 🐻 State-Management for vue (vue3 / vue2) based on zustand.
  • zustand-pub - 🐻 Cross-Application/Cross-Framework state management and sharing in iframe, micro-front, module federation, componentization, etc for vue / react.
  • route-resource-preload - 🚀 Focus on improving the first screen loading speed of applications and providing the best user experience, inspiration comes from the preloading of NextJS.
  • image-color-utils - 🛠 Image color processing tool, operate image pixels, such as pickup color, contrast color similarity, etc.
  • vue-waterfall2 - 🧩 Waterfall adaptive plugin for vue and support lazy load, so easy!

If you have any developed questions, Welcome to communicate with me on WeChat.

blog's People

Contributors

awesomedevin avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

blog's Issues

【NextJS】一文了解 NextJS 并提升应用性能的最佳实践

引言

从本文中,我将从是什么为什么怎么做来为大家阐述 NextJS 以及如何优化 NextJS 应用体验。

一、NextJS是什么

NextJS是一款基于 React 进行 web 应用开发的框架,它以极快的应用体验而闻名,内置 Sass、Less、ES 等特性,开箱即用。SSR 只是 NextJS 的一种场景而已,它拥有4种渲染模式,我们需要为自己的应用选择正确的渲染模式:

  • Client Side Rendering (CSR)
    客户端渲染,往往是一个 SPA(单页面应用),HTML文件仅包含JS\CSS资源,不涉及页面内容,页面内容需要浏览器解析JS后二次渲染。
  • Static Site Generation (SSG)
    静态页面生成,对于不需要频繁更新的静态页面内容,适合SSR,不依赖服务端。
  • Server Side Rendering (SSR)
    服务端渲染,对于需要频繁更新的静态页面内容,更适合使用SSR,依赖服务端。
  • IncreIncremental Site Rendering (ISR)
    增量静态生成,基于页面内容的缓存机制,仅对未缓存过的静态页面进行生成,依赖服务端。

SSG / ISR 都是非常适合博客类应用的,区别在于SSG是构建时生成,效率较低,ISR是基于已有的缓存按需生成,效率更高

image.png

二、为什么选 NextJS

优点:

  1. 首屏加载速度快
    我们的内嵌场景比较丰富,因此比较追求页面的一个首屏体验,NextJS 的产物类似 MPA(多页面应用),在请求页面时会对当前页面的资源进行按需加载,而不是去加载整个应用, 相对于 SPA 而言,可以实现更为极致的用户体验。

  2. SEO优化好
    SSR \ SSG \ ISR 支持页面内容预加载,提升了搜索引擎的友好性。

  3. 内置特性易用且极致
    NextJS 内置 getStaticPropsgetServerSidePropsnext/imagenext/linknext/script等特性,充分利用该框架的这些特性,为你的用户提供更高层次的体验,这些内容后文会细讲。

缺点:

  1. 页面响应相对于SPA而言更慢
    由于页面资源分页面按需加载,每次路由发生变化都需要加载新的资源,优化不够好的话,会导致页面卡顿
  2. 开发体验不够友好
    开发环境下 NextJS 根据当前页面按需进行资源实时构建,影响开发及调试体验

三、如何将 NextJS 应用体验提升到极致

作为一名开发者,我们追求的不应该是应用能用就好,而是好用,那么如何评价我们的应用是否好用呢?

  • 最直接的方案当然是通过收集用户反馈来评判
  • 从开发层面,最直观的就是通过performancelighthouse来评判

3.1 优化前

如你所见,由于应用模块的一个复杂性,我们的 NextJS 应用起初性能并不是很好,甚至谈得上是差

  • FCP: 首次内容绘制时间1.8s

    image.png

  • lighthouse: 性能评分报告 55分,Time to Interactive(TTI) 可交互时间为 7.3s,通常是发生在页面依赖的资源已经加载完成。

  • network: 我们每次进行路由跳转都要按需加载资源,因此我们需要单个页面的 DomContentLoaded/Load 尽可能快。

    image.png

  • 页面构建时间

    image.png

这些指标都间接反馈出应用的体验问题亟待解决。

3.2 优化措施

  • 优化用户体验

    • 1. 开启 gzip 压缩
      可以发现请求的size居然上M了,“离离原上普”
      image.png
      优化后可以看到, 压缩效果还是很明显的
      image.png

    • 2. 针对非首屏组件基于 dynamic 动态加载
      在页面加载过程中,针对一些不可见组件,我们应该动态导入,而不是正常导入,确保只有需要该组件的场景下,才 fetch 对应资源, 通过 next/dynamic,在构建时,框架层面会帮我们进行分包

      import dynamic from 'next/dynamic'
      const Modal = dynamic(() => import('../components/mModal'));
      export default function Index() {
        return (
          {showModal && <Modal />}
        )
      }

      打开Network。当条件满足时,你将看到一个新的网络请求被发出来获取动态组件(单击按钮打开一个模态)。

    • 3 . next/script 优化 script 加载时
      next/script 可以帮助我们来决定 js 脚本加载的时机

      strategy 描述
      beforeInteractive 可交互前加载脚本
      afterInteractive 可交互后加载脚本
      lazyOnload 浏览器空闲时加载脚本
      <Script strategy="lazyOnload" src="//wl.jd.com/boomerang.min.js" />
    • 4. next/image 优化图片资源
      next/image 可帮助我们对图片进行压缩(尺寸 or 质量),且支持图片懒加载,默认 loader 依赖 nextjs 内置服务,也可以通过{loader: custom}自定义loader

      import Image from 'next/image'
      const myLoader = ({ src, width, quality }) => {
        return `https://example.com/${src}?w=${width}&q=${quality || 75}`
      }
      const MyImage = (props) => {
        return (
          <Image
            loader={myLoader}
            src="me.png"
            alt="Picture of the author"
            width={500}
            height={500}
          />
        )
      }
    • 5. next/link 预加载
      基于 hover 识别用户意图,当用户 hover 到 Link 标签时,对即将跳转的页面资源进行预加载,进一步防止页面卡顿

      import Link from 'next/link'
      <Link prefetch={false} href={href}>目标页面</Link>
    • 6. 静态内容预加载
      基于 getStaticProps 对不需要权限的内容进行预加载,它将在 NextJS 构建时被编译到页面中,减少了 http 请求数量

      function Blog({ posts }) {
        return (
          <ul>
            {posts.map((post) => (
              <li>{post.title}</li>
            ))}
          </ul>
        )
      }
      export async function getStaticProps() {
        const res = await fetch('https://.../posts')
        const posts = await res.json()
      
        return {
          props: {
            posts,
          },
        }
      }
      export default Blog
    • 7. 第三方 library 过大时,基于 umd 按需加载
      第三方 library 过大时,以 umd 进行引入,在需要的场景下通过 script 进行加载。

      // 封装记载umd模块的hoc
      function loadUmdHoc(Comp: (props) => JSX.Element, src: string) {
        return function Hoc(props) {
          const [isLoaded, setLoaded] = useState(
            !!Array.from(document.body.getElementsByTagName('script')).filter(
              (item) => item.src.match(src)
            ).length
          )
          useEffect(() => {
            if (isLoaded) return
            const script = document.createElement('script')
            script.src = src
            script.onload = () => {
              setLoaded(true)
            }
            document.body.append(script)
          }, [])
      
          if (isLoaded) {
            return <Comp {...props} />
          }
          return <></>
        }
      }
      
      function Upload(){
        // todo 使用umd模块
        return <></>
      }
      
      // 使用该组件时,加载hoc
      export default loadUmdHoc(
        Upload,
        'xxx.umd.js'
      )
  • 优化研发体验

    • 1. 基于 urlimport 进行瘦身,提升编译效率
      urlImport 是 NextJS 提供的一个实验特性,支持加载远程 esmodule
      image.png
      NextJS 会在本地对所加载的远程模块进行缓存, 减少了我们所需构建的模块数,缺点是它会影响 treeShaking 的一个效果,因此在生产环境,建议通过NormalModuleReplacementPlugin对 urlimport 的依赖进行一个本地替换

    image.png

    • 2. webpack 配置选择性忽略
      针对一些生成环境的配置我们可以通过区分环境来进行选择性忽略部分配置,如 module federation exposes 在开发环境我们就可以忽略掉。

      dev.conf.js
      image.png
      pro.conf.js
      image.png

    • 3. 开启 SWC 编译
      SWC 是基于 Rust 实现的一款开发工具,既可用于编译也可用于打包,据官方言,它比 Babel 快了 20~70倍,NextJS 在 12 版本默认打开了 SWC 的支持。开启 SWC 后,应用的编译速度将比 Babel 快 17 倍,刷新速度快 5 倍。需要注意的是如果你通过.babelrc自定义 babel 配置,SWC 的一些特性将会被关闭。

3.3 优化后

从以下指标可以看出我们应用的体验得到了很大提升, 实际的一个交互体验也好了不少,在路由跳转上实现了类似 SPA 的一个体验,不会再出现页面卡顿的情况。

  • FCP: 首次内容绘制时间 从 1.8s 优化到 0.35s,提升了近 80%

    image.png

  • lighthouse: 评分从55提升到了80,TTI 从7.3s 优化到了 2.6s, 分别提升了 30% / 64%,chrome 的最佳实践分达到了满分💯

  • network: DomContentLoaded 从 2.42s 优化到 0.68s,Load从 3.77s 优化到 1.47s ,分别提升了 77% / 61%

    image.png

  • 页面构建时间: 基本满足了毫秒级实现页面编译的需求,提升了 70% 以上

    image.png

四、后续规划

为了实现更为极致的用户体验,我们后续计划将资源上CDN,减少Waiting for server response的性能损耗,并加入PWA的离线缓存特性。

参考文章
Optimize Next.js App Bundle and Improve Its Performance
我看Next.js:一个更现代的海王

【Javascript】深入理解this作用域问题

理解this作用域

《javascript高级程序设计》中有说到:

this对象是在运行时基于函数的执行环境绑定的:在全局函数中,this等于window ,而当函数被作为某个对象调用时,this等于那个对象。不过,匿名函数具有全局性,因此this对象同常指向window

不过,在全局函数中,this等于window,匿名函数具有全局性,因此this对象通常指向window,针对于匿名函数this具有全局性的观点仍是有争议的,可参考 https://www.zhihu.com/question/21958425

this的指向取决于函数(不包含箭头函数)执行时的环境

验证过程如下:

关于闭包经常会看到这么一道题:

var name = "The Window";
    var object = {
        name : "My Object",
        getNameFunc : function(){
            return function(){
                return this.name;
            };
        }
    };
console.log(object.getNameFunc()());//result:The Window


在这里,getNameFunc return了1个匿名函数,可能你会认为这就是输出值为The Window的原因

但是,我们再来尝试写1个匿名函数

var name = "The Window";
 var object = {
  name : "My Object",
  getNameFunc : function(){
   return this.funAA;
  },
  funAA:function(){
   return this.name
  }
 };
 console.log(object.getNameFunc()(),object.funAA())


可以发现,同样是匿名函数,却输出了The Window, My Object

在作用域链中,执行函数时会创建一个称为“运行期上下文(execution context)”的内部对象,运行期上下文定义了函数执行时的环境。

因为函数在全局作用域中被object.getNameFunc()独立调用,funAA的作用域链被初始化为undefined即window的[[Scope]]所包含的对象,导致输出结果为window.name

对作用域链不是很了解的同学,可以查看这边文章【Javascript】深入理解javascript作用域与作用域链

实践是检验真理的唯一标准,让我们用代码测试一下

var name = "The Window";
 var object = {
  name : "My Object",
  getNameFunc : function(){
   return this.funAA();
  },
  funAA:function(){
   return this.name
  }
 };
console.log(object.getNameFunc(),object.funAA())


可以发现,输出了 My Object, My Object
getNameFunc仍为匿名函数,但是return的是this.funAA(),此时,this.funAA变成了由object调用,验证了我们之前的猜想:

函数执行环境影响了this作用域,对这个demo的代码不太理解的同学,可以看一下另一个比较简单的案例

this.x = 9;   
var module = {
  x: 81,
  getX: function() { console.log(this.x) }
};

module.getX(); // 81

var retrieveX = module.getX;
function A(){
  this.x = 22;
  retrieveX() //22
}
A()

new运算符对this作用域的影响

还是实践出真理,我们先来写一段代码

var a = 2
function test(){
    this.a = 1
    console.log(window.a)
}
new test()
test()


可以看出输出结果为2,1
new运算符改变了test函数内this的作用域,改变的原理是通过在函数内创建一个对象obj,并通过test.call(obj),执行obj.test(),call函数原理:

Function.prototype.call1 = function(obj,...args){
	obj.fn = this
	obj.fn(...args)
	delete obj.fn
}

这样test函数被对象obj调用,test复制的是obj的作用域链,而不是window

function subNew(){
    var obj = {}
    var res = test.call(obj,...arguments)
}
subNew()   // 作用等于new test()

let/var/const对this作用域的影响

继续写代码通过事实来说明

var a = 1 // 全局作用域
let b = 1   // 块级作用域
const c = 1   // 块级作用域
function foo(){
  var d = 1  // 函数作用域
  this.a = 2
  this.b = 2
  this.c = 2
  this.d = 2
  console.log(a,b,c,d) // 2,1,1,1
}
foo()

a为全局作用域中的变量,可以被this对象访问,b/c/d则不行

可以发现,全局作用域中的a变量被改变,b变量与c变量都没有被改变,说明在fn()中通过this访问不到window作用域中的b/c变量
注:这里说的访问不到与const定义的变量是常量没有关系,因为如果访问到的话,是会报typeError的

箭头函数对this作用域的影响

var num = 1
const object = {
  num:2,
  foo: function(){
    return ()=>{
      console.log(this.num)
    }
  }
}
object.foo()()  // 2

箭头函数 this 指向 所处环境的上下文的 this 值,与是否独立调用或作为属性被调用,没有关系。
箭头函数没有arguments/prototype,不能作为构造函数,不能使用new

总结

  1. this的指向取决于函数执行时所创建运行期上下文(execution context)的内部对象,它与当前运行函数的[[scope]]所包含的对象组成了1个新的对象,这个对象就是活动对象,然后此对象会被推入作用域链的前端
  2. 如果调用的函数,被某一个对象所拥有,那么该函数在调用时,内部的this指向该对象。
  3. this指向与匿名函数没有关系,如果函数在全局作用域window中被独立调用,那么该函数内部的this,则指向undefined。但是在非严格模式中,当this指向undefined时,它会被自动指向全局对象。
  4. 在函数被独立调用时,并处于非严格模式下,函数内的this对象有能力也仅能访问到全局作用域中定义的变量即window对象, 块级作用域/函数作用域内的变量都无法被访问
  5. 箭头函数 this 指向 所处环境的上下文的 this 值,与是否独立调用或作为属性被调用,没有关系。

相关知识点

不理解new的实践可以查看我的这篇文章【Javascript】彻底捋清楚javascript中 new 运算符的实现
对作用域链不是很了解的同学,可以查看这边文章【Javascript】深入理解javascript作用域与作用域链

【Javascript】使用react hooks 构建 redux 进行状态管理

前言

我们在做一个大型的复杂应用时,往往有很多数据会在多个页面多个组件中同时被使用,这时如果仍然使用props传参的方式,就会显得组件之间耦合度过高,且开发效率低

为了解决这个问题,2014年 Facebook 提出了 Flux 架构的概念,引发了很多的实现。2015年,Redux 出现,将 Flux 与函数式编程结合一起,很短时间内就成为了最热门的前端架构。

目录

  • React Hooks
  • Hooks API
  • Hooks 构建 Redux
  • 完整DEMO

React Hooks

hooks 是react 16.8 引入的特性,他允许你在不写class的情况下操作state 和react的其他特性。
hooks 只是多了一种写组件的方法,使编写一个组件更简单更方便,同时可以自定义hook把公共的逻辑提取出来,让逻辑在多个组件之间共享。

Hooks API

基础Hooks

  • useState - 声明state变量
const { state, setState } = useState(initialState)
  • useEffect - Effect钩子允许你在函数组件中执行副作用
useEffect(didUpdate,[])   //didUpdate为要做的更新,[]为要监听的变量
  • useContext - 接收一个Context对象(React.createContext返回的值)并且返回当前<MyContext.Provider>的值, 返回值取决于最近的 <MyContext.Provider>
const  store = useContext(Context)

附加Hooks

  • useReducer - 接收一个(state, action) => newState类型的reducer,并且返回一个与dispatch方法配对的state对象(如果你熟悉redux,那么你已经会了)
const [state,dispatch] = useReducer(reducers,initialState)
  • useCallback - 类似userEffect,与前者相比不能执行副作用,返回缓存的函数
  • useMemo - 类似userEffect,与前者相比不能执行副作用,返回缓存的变量
  • useRef - 类似vue中的$refs,用于操作dom
  • useImperativeHandle - 因为函数式组件不存在组件实例,所以正常情况下ref无法调用子组件的函数,useImperativeHandle返回了一个回调函数帮助我们解决此类问题
function Com(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
Com = forwardRef(Com);
  • useLayoutEffect - 类似componentDidMount/Update, componentWillUnmount,会在dom元素更新后立即执行回调函数
  • useDebugValue - 用于自定义Hooks

Hooks 构建 Redux

Redux 规定,将模型的更新逻辑全部集中于一个特定的层(Flux 里的 store,Redux 里的 reducer)。Flux 和 Redux 都不允许程序直接修改数据,而是用一个叫作 “action” 的普通对象来对更改进行描述。

需要用到的API有createContext,useContext,useReducer

step1 - 首先,我们创建state

state.js

export default  {
  username:''
}

step2 - Redux 规定不允许程序直接修改数据,而是用一个叫作 action 的普通对象来对state进行更改

action.js

export  default {
  setUserName:function(payload){
    return {
      type:'setUserName',
      payload
    }
  }
}

step3 - 将模型的更新逻辑全部集中于一个特定的层(Flux 里的 store,Redux 里的 reducer

reducer.js

export default function reducer(state,action){
  const { payload } = action
  switch(action.type)
  {
    case 'setUserName':     // 匹配action中的type,return新的state
      return {
        ...state, 
        username: payload
      }
    default:
      return state
  }
}

step4 - 使用useReducer获取state以及用于事件派发更新state的dispatch函数

index.js

const [state,dispatch] = useReducer(reducers,initialState)

step5 - 允许所有子组件访问state及dispatch

这里我们需要排除props传参的方式,因为这样就失去了使用redux进行状态管理的意义,这里就需要用到Context
index.js

import React,{createContext, useReducer} from 'react'
import reducers from './reducers'
import initialState from './state'

export const Context = createContext({})
export const Consumer =  Context.Consumer
export function Provider(props){
  const [state,dispatch] = useReducer(reducers,initialState)
  const store = { state, dispatch } 
  return (
  <Context.Provider value={ store }>
    {props.children}
  </Context.Provider>
  )
}

这里将useReducer的返回值放到Provider的value中,子组件通过ConsumeruseContext就能访问到了

整个Redux由state、action、reducer、index构成

完整DEMO

1. index.js - 将组件放置在中

import React from 'react'
import ReactDOM from 'react-dom'
import App from './App';
import { Provider } from './redux'

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

2. app.js - 将组件放置在中,并更新state

import React, { useContext } from 'react';
import { AppContext } from './redux'
import Child from './child'
import actions from './redux/actions'

function App() {
  const { dispatch } = useContext( AppContext )   // 获取事件派发函数
  function handleInput(e){
    dispatch(actions.setUserName(e.target.value))   // 派发更新事件并传值
  }
  return (
    <div className="App">
        <input onInput={handleInput} type='text'  />
        <Child  />
    </div>
  );
}
export default App;

3. child.js - 获取Redux中的username

import React, { useContext } from 'react';
import { AppContext } from './redux'

export default function Children(){
  const { state } = useContext(AppContext)
  return (
    <div>Name:{state.username}</div>
  )
}
  1. DEMO源码

【Javascript】Event loop 在浏览器端与NodeJS中的差别 以及 NodeJS中关于setTimeout与setImmediate引发的问题

组内每周都会有分享总结会,昨晚分享的课题是Event Loop,我很积极且有点自信地答了几道题,结果被虐的体无完肤,果然有些东西不经常回顾就容易忘,于是花一晚上挑灯夜战重新做了一份有关event loop的知识总结,在此分享给大家,希望对各位看官有所帮助,看完有收获的同学还请积极点赞,讲的不对的地方,望指出,我会及时修正,谢谢~

浏览器端

浏览器端的event loop基于javascript中的堆/栈/任务队列,任务队列又分为宏任务微任务

每次事件循环的时候:
  • 微任务/宏任务在相同作用域下,会先执行微任务,再执行宏任务
  • 宏任务处于微任务作用域下,会先执行微任务,再执行微任务中的宏任务
  • 微任务处于宏任务作用域下时,会先执行宏任务队列中的任务,然后再执行微任务队列中的任 务,在当前的微任务队列没有执行完成时,是不会执行下一个宏任务的。

本文主要讲解的还是Node,对于浏览器端event loop的具体分析及证明可以查看这篇文章探究javascript中的堆/栈/任务队列与并发模型 event loop的关系

Nodejs端

nodejs 的事件循环分为6个阶段,每个阶段都有1个任务队列,微任务在事件循环的各个阶段之间执行

image

timers 阶段: 这个阶段执行timer(setTimeout、setInterval)的回调

一个timer指定一个下限时间而不是准确时间,在达到这个下限时间后执行回调。在指定的时间过后,timers会尽早的执行回调,但是系统调度或者其他回调的执行可能会延迟它们。

从技术上来说,poll阶段控制timers什么时候执行,而执行的具体位置在timers
下限的时间有一个范围:[1, 2147483647],如果设定的时间不在这个范围,将被设置为1。

I/O callbacks 阶段:执行大多回调以及一些系统调用错误,比如网络通信的错误回调

idle, prepare 阶段: 仅node内部使用

poll 阶段:获取新的I/O事件, 适当的条件下node将阻塞在这里

poll阶段有两个主要的功能:
1. 是执行下限时间已经达到的timers的回调
2. 是处理poll队列里的事件。

注:Node很多API都是基于事件订阅完成的,这些API的回调应该都在poll阶段完成。
当事件循环进入poll阶段:

  • poll队列不为空的时候,事件循环肯定是先遍历队列并同步执行回调,直到队列清空或执行回调数达到系统上限。

  • poll队列为空的时候,这里有两种情况。

    • 如果代码已经被setImmediate()设定了回调,那么事件循环直接结束poll阶段进入check阶段来执行check队列里的回调。

    • 如果代码没有被设定setImmediate()设定回调:

      • 如果有被设定的timers,那么此时事件循环会检查timers,如果有一个或多个timers下限时间已经到达,那么事件循环将绕回timers阶段,并执行timers的有效回调队列。
      • 如果没有被设定timers,这个时候事件循环是阻塞在poll阶段等待回调被加入poll队列。

check 阶段:执行 setImmediate() 的回调

这个阶段允许在poll阶段结束后立即执行回调。如果poll阶段空闲,并且有被setImmediate()设定的回调,那么事件循环直接跳到check执行而不是阻塞在poll阶段等待回调被加入。

注:事件循环运行到check阶段的时候,setImmediate()具有最高优先级,只要poll队列为空,代码被setImmediate(),无论是否有timers达到下限时间,setImmediate()的代码都先执行

close callbacks 阶段:执行 socket 的 close 事件回调

如果一个sockethandle被突然关掉(比如socket.destroy()),close事件将在这个阶段被触发,否则将通过process.nextTick()触发。

NodeJS中关于setTimeout与setImmediate引发的问题

问题引入

setTimeout(()=>{
    console.log('timer')
})
setImmediate(()=>{
    console.log('immediate')
})


可以发现结果存在随机性

原因

首先进入的是timers阶段,如果我们的机器性能一般,那么进入timers阶段,一毫秒已经过去了,那么setTimeout的回调会首先执行。

如果没有到一毫秒,那么在timers阶段的时候,下限时间没到,setTimeout回调不执行,事件循环来到了poll阶段,这个时候队列为空,此时有代码被setImmediate(),所以进入check阶段,先执行了setImmediate()的回调函数,之后在下一个事件循环再执行setTimemout的回调函数。

而我们在执行代码的时候,进入timers的时间延迟其实是随机的,并不是确定的,所以会出现两个函数执行顺序随机的情况。

我们再来看一段代码

fs.readFile('./main.js',()=>{
    setTimeout(()=>{
        console.log('timer')
    })
    setImmediate(()=>{
        console.log('immediate')
    })
})


可以发现setImmediate永远先于setTimeout执行

原因

fs.readFile的回调是在poll阶段执行的,回调执行完毕后poll阶段的队列为空,于是进入check阶段,执行setImmediate回调,而setTimeout的回调需要等到下一个事件循环的timers阶段才去执行

NodeJS中的process.nextTick() and Promise

对于这两个,我们可以把它们理解成一个微任务。也就是说,它其实不属于事件循环的一部分。

那么他们是在什么时候执行呢?

不管在什么地方调用,他们都会在其所处的事件循环最后,在事件循环进入下一个循环的阶段前执行,但是nextTick优先于promise执行。
process.nextTick() 会在各个事件阶段之间执行,一旦执行,要直到nextTick队列被清空,才会进入到下一个事件阶段,所以如果递归调用 process.nextTick()/promise,会导致出现I/O starving(饥饿)的问题,推荐使用setImmediate()

看了这么多,现在大家做两道题吧,检测自己是否真的理解了

question1

setTimeout(() => {
  console.log('timeout1')
  Promise.resolve().then(()=>{
        console.log('reslove1')
    })
})

setTimeout(() => {
  console.log('timeout2')
  Promise.resolve().then(()=>{
        console.log('reslove2')
    })
})

setImmediate(()=>{
    console.log('setImmediate1')
})

setImmediate(()=>{
    console.log('setImmediate2')
})

question2

setTimeout(() => {
  console.log('timeout1')
  Promise.resolve().then(()=>{
        console.log('reslove1')
    })
})

setTimeout(() => {
  console.log('timeout2')
  Promise.resolve().then(()=>{
        console.log('reslove2')
    })
})

setImmediate(()=>{
    console.log('setImmediate1')
})

setImmediate(()=>{
    console.log('setImmediate2')
})

Promise.resolve('resolve3').then((data)=>{
    console.log(data)
})

【算法-简单】合并两个有序链表

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} list1
 * @param {ListNode} list2
 * @return {ListNode}
 */
var mergeTwoLists = function(list1, list2) {
    if(!list1) return list2
    if(!list2) return list1

    if(list1.val < list2.val){
        list1.next = mergeTwoLists(list1.next, list2)
        return list1
    }else{
        list2.next = mergeTwoLists(list1, list2.next)
        return list2
    }
}

【算法系列 - 剑指Offer】重建二叉树

题目

输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。

JS实现

function reConstructBinaryTree(pre, vin)
{
    var result = null
    if(pre.length>1)
    {
        result = {
            val:pre[0]
        }
        const index = vin.indexOf(pre[0])
        var vinLeft = vin.slice(0,index)
        var vinRight = vin.slice(index+1)
        pre.shift()
        var preLeft = pre.slice(0,vinLeft.length)
        var preRight = pre.slice(vinLeft.length)
        result = {
            left: reConstructBinaryTree(preLeft,vinLeft),
            right:reConstructBinaryTree(preRight,vinRight),
            val:result.val
        }
    }
    else if(pre.length === 1)
    {
        result = {
            left:null,
            right:null,
            val:pre[0]
        }
    }
    return result
}

【Javascript】IOC/DI原理分析,并实现一个 简易版 IOC/DI 框架

本文基于自身理解进行输出,目的在于交流学习,如有不对,还望各位看官指出。

DI

DI—Dependency Injection,即“依赖注入”:对象之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个对象注入到对象属性之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升对象重用的频率,并为系统搭建一个灵活、可扩展的框架。

使用方式

首先看一下常用依赖注入 (DI)的方式:

function Inject(target: any, key: string){
    target[key] = new (Reflect.getMetadata('design:type',target,key))()
}

class A {
    sayHello(){
        console.log('hello')
    }
}

class B {
    @Inject   // 编译后等同于执行了 @Reflect.metadata("design:type", A)
    a: A

    say(){
       this.a.sayHello()  // 不需要再对class A进行实例化
    }
}

new B().say() // hello

原理分析

TS在编译装饰器的时候,会通过执行__metadata函数多返回一个属性装饰器@Reflect.metadata,它的目的是将需要实例化的service以元数据'design:type'存入reflect.metadata,以便我们在需要依赖注入时,通过Reflect.getMetadata获取到对应的service, 并进行实例化赋值给需要的属性。

@Inject编译后代码:

var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};

// 由于__decorate是从右到左执行,因此, defineMetaData 会优先执行。
__decorate([
    Inject,
    __metadata("design:type", A)  //  作用等同于 Reflect.metadata("design:type", A)
], B.prototype, "a", void 0);

即默认执行了以下代码:

Reflect.defineMetadata("design:type", A, B.prototype, 'a');

Inject函数需要做的就是从metadata中获取对应的构造函数并构造实例对象赋值给当前装饰的属性

function Inject(target: any, key: string){
    target[key] = new (Reflect.getMetadata('design:type',target,key))()
}

不过该依赖注入方式存在一个问题:

  • 由于Inject函数在代码编译阶段便会执行,将导致B.prototype在代码编译阶段被修改,这违反了六大设计原则之开闭原则(避免直接修改类,而应该在类上进行扩展)
    那么该如何解决这个问题呢,我们可以借鉴一下TypeDI的**。

typedi - typedi 是一款支持TypeScript和JavaScript依赖注入工具

typedi 的依赖注入**是类似的,不过多维护了一个container

1. metadata

在了解其container前,我们需要先了解 typedi 中定义的metadata,这里重点讲述一下我所了解的比较重要的几个属性。

  • id: service的唯一标识
  • type: 保存service构造函数
  • value: 缓存service对应的实例化对象
const newMetadata: ServiceMetadata<T> = {
      id: ((serviceOptions as any).id || (serviceOptions as any).type) as ServiceIdentifier,    // service的唯一标识
      type: (serviceOptions as ServiceMetadata<T>).type || null,  // service 构造函数
      value: (serviceOptions as ServiceMetadata<T>).value || EMPTY_VALUE,  // 缓存service对应的实例化对象
};

2. container 作用

function ContainerInstance() {
        this.metadataMap = new Map();  //保存metadata映射关系,作用类似于Refect.metadata
        this.handlers = []; // 事件待处理队列
        get(){};  // 获取依赖注入后的实例化对象
         ...
}
  • this. metadataMap - @service会将service构造函数以metadata形式保存到this.metadataMap中。
    • 缓存实例化对象,保证单例;
  • this.handlers - @inject会将依赖注入操作的对象目标行为以 object 形式 push 进 handlers 待处理数组。
    • 保存构造函数静态类型属性间的映射关系。
{
        object: target,  // 当前等待挂载的类的原型对象
        propertyName: propertyName,  // 目标属性值
        index: index, 
        value: function (containerInstance) {   // 行为
            var identifier = Reflect.getMetadata('design:type', target, propertyName)
            return containerInstance.get(identifier);
        }
}

@inject将该对象 push 进一个等待执行的 handlers 待处理数组里,当需要用到对应 service 时执行 value函数 并修改 propertyName。

if (handler.propertyName) {
     instance[handler.propertyName] = handler.value(this);
}
  • get - 对象实例化操作及依赖注入操作
    • 避免直接修改类,而是对其实例化对象的属性进行拓展;

相关结论

  • typedi中的实例化操作不会立即执行, 而是在一个handlers待处理数组,等待Container.get(B),先对B进行实例化,然后从handlers待处理数组取出对应的value函数并执行修改实例化对象的属性值,这样不会影响Class B 自身
  • 实例的属性值被修改后,将被缓存到metadata.value(typedi 的单例服务特性)。

相关问题案例可查看: https://stackoverflow.com/questions/55684776/typedi-inject-doesnt-work-but-container-get-does

new B().say()  // 将会输出sayHello is undefined

Container.get(B).say()  // hello word

实现一个简易版 DI Container

此处代码依赖TS,不支持JS环境

interface Handles {
    target: any
    key: string,
    value: any
}

interface Con {
    handles: Handles []   // handlers待处理数组
    services: any[]  // service数组,保存已实例化的对象
    get<T>(service: new () => T) : T   // 依赖注入并返回实例化对象
    findService<T>(service: new () => T) : T  // 检查缓存
    has<T>(service: new () => T) : boolean  // 判断服务是否已经注册
}

var container: Con = {
    handles: [],  // handlers待处理数组
    services: [], // service数组,保存已实例化的对象
    get(service){
        let res: any = this.findService(service)
        if(res){
            return  res
        }

        res = new service()
        this.services.push(res)
        this.handles.forEach(handle=>{
            if(handle.target !== service.prototype){
                return
            }
            res[handle.key] = handle.value
        })
        return res
    },

    findService(service){
        return this.services.find(instance => instance instanceof service)
    },

   // service是否已被注册
    has(service){
        return !!this.findService(service)
    }
}

function Inject(target: any, key: string){
    const service = Reflect.getMetadata('design:type',target,key)
    
    // 将实例化赋值操作缓存到handles数组
    container.handles.push({
        target,
        key,
        value: new service()
    })

    // target[key] = new (Reflect.getMetadata('design:type',target,key))()
}

class A {
    sayA(name: string){
        console.log('i am '+ name)
    }
}

class B {
    @Inject
    a: A

    sayB(name: string){
       this.a.sayA(name)
    }
}

class C{
    @Inject
    c: A

    sayC(name: string){
       this.c.sayA(name)
    }
}

// new B().sayB(). // Cannot read property 'sayA' of undefined
container.get(B).sayB('B')
container.get(C).sayC('C')

【javascript】Typescript 装饰器 底层原理分析

装饰器种类

// 方法装饰器 - 如果方法装饰器返回一个值,它会被用作方法的属性描述符。
function decorateMethod(target: any,key: string,value: any){
    return{
        value: function(...args: any[]){
            console.log('target',target) // 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
            console.log('target === C.prototype',target === C.prototype) // true
            console.log('key',key) // 方法名
            console.log('value',value)  // 成员的属性描述符 Object.getOwnPropertyDescriptor
            console.log('args',args)
            var a = args.map(a => JSON.stringify(a)).join();
            var result = value.value.apply(this, args);
            var r = JSON.stringify(result);
            console.log(`Call: ${key}(${a}) => ${r}`);
            return result;
        }
    }
}

// 类装饰器  - 如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。
function decorateClass(str: string){
    return function(constructor: Function){ 
        console.log(constructor) // 参数为类的构造函数
    }
}

// 访问器装饰器 - 如果访问器装饰器返回一个值,它会被用作方法的属性描述符。
function configurable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.configurable = value;
    };
}

// 装饰器工厂 - 闭包
function format(formatString: string) {
    return Reflect.metadata(formatMetadataKey, formatString);
}

// 属性装饰器 - 返回值也会被忽略。因此,属性描述符只能用来监视类中是否声明了某个名字的属性
function getFormat(target: any, propertyKey: string) {
     console.log('target') //对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
     console.log('propertyKey') // 属性名
    return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}

// 参数装饰器 - 参数装饰器的返回值会被忽略。
function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
     console.log('target') // //对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
     console.log('propertyKey') // 参数名
     console.log('parameterIndex')  // 参数在函数参数列表中的索引。
    let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
    existingRequiredParameters.push(parameterIndex);
    Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey);
}


@decorateClass('decorateClass')
class C{

		@format("Hello, %s")
    great: string
    
    @decorateMethod('decorateMethod')
    sum(@required x: number,y: number){
        return x + y
    }

    @configurable(false)
    get x() { return this._x; }
}

方法日志装饰器底层实现 - __decorate()

首先通过ast将我们针对相同成员的装饰器处理为一个数组,ts规范规定装饰器工厂函数从上至下开始执行装饰器函数从下至上开始执行,__decorate() 会通过reduce.right从右到左依次执行这个数组中的函数。基于ts的规范不同的装饰器提供的参数不同,所以ts代码被编译成js代码后,__decorate 需要根据入参数长度进行处理,执行不同的装饰器函数时传入不同的参数。

方法装饰器函数以及访问器装饰器有返回值会将返回值通过defineProperty设置为函数本身的value,函数没有返回值的话,则通过 Object.getOwnPropertyDescriptor 获取函数自身的 value,并通过defineProperty设置属性value,之所以会有defineProperty的步骤,是为了方便某些场景我们在装饰器中需要与函数本身进行耦合,如:log及报错捕捉。通过defineProperty,装饰器的逻辑可以做到与函数本身的逻辑进行耦合。

var __decorate =
  (this && this.__decorate) ||
  function (decorators, target, key, desc) {
    var c = arguments.length,
      r =
        c < 3
          ? target
          : desc === null
          ? (desc = Object.getOwnPropertyDescriptor(target, key))
          : desc,
      d;
    console.log('r',r)
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
      r = Reflect.decorate(decorators, target, key, desc);
    else
      for (var i = decorators.length - 1; i >= 0; i--)
        if ((d = decorators[i]))
          r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    console.log(c)
    return c > 3 && r && Object.defineProperty(target, key, r), r;
  };

var __metadata =
  (this && this.__metadata) ||
  function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function")
      return Reflect.metadata(k, v);
  };

var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};
// You could think of it in three ways:
//
//  - A place to learn TypeScript in a place where nothing can break
//  - A place to experiment with TypeScript syntax, and share the URLs with others
//  - A sandbox to experiment with different compiler features of TypeScript
// To learn more about the language, click above in "Examples" or "What's New".
// Otherwise, get started by removing these comments and the world is your playground.
// 方法装饰器 - 如果方法装饰器返回一个值,它会被用作方法的属性描述符。
function decorateMethod(str) {
    return function (target, key, value) {
        return {
            value: function (...args) {
                console.log('decorateMethod', str); // decorateMethod
                console.log('target', target); // 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
                console.log('target === C.prototype', target === C.prototype); // true
                console.log('key', key); // 方法名
                console.log('value', value); // 成员的属性描述符 Object.getOwnPropertyDescriptor
                console.log('args', args);
                var a = args.map(a => JSON.stringify(a)).join();
                var result = value.value.apply(this, args);
                var r = JSON.stringify(result);
                console.log(`Call: ${key}(${a}) => ${r}`);
                return result;
            }
        };
    };
}
// 类装饰器  - 如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。
function decorateClass(str) {
    return function (constructor) {
        console.log(constructor); // 参数为类的构造函数
    };
}
// 访问器装饰器 - 如果访问器装饰器返回一个值,它会被用作方法的属性描述符。
function configurable(value) {
    return function (target, propertyKey, descriptor) {
        descriptor.configurable = value;
    };
}
// 装饰器工厂 - 闭包
function format(formatString) {
    return Reflect.metadata('formatMetadataKey', formatString);
}
// 属性装饰器 - 返回值也会被忽略。因此,属性描述符只能用来监视类中是否声明了某个名字的属性
function getFormat(target, propertyKey) {
    console.log('target'); //对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
    console.log('propertyKey'); // 属性名
    return Reflect.getMetadata('formatMetadataKey', target, propertyKey);
}
// 参数装饰器 - 参数装饰器的返回值会被忽略。
function required(target, propertyKey, parameterIndex) {
    console.log('target'); // //对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
    console.log('propertyKey'); // 参数名
    console.log('parameterIndex'); // 参数在函数参数列表中的索引。
}
let C = class C {
    constructor() {
        this.str = 'string';
    }
    sum(x, y) {
        return x + y;
    }
    get x() { return this.str; }
};
__decorate([
    format("Hello, %s"),
    __metadata("design:type", String)
], C.prototype, "str", void 0);
__decorate([
    decorateMethod('decorateMethod'),
    __param(0, required),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Number, Number]),
    __metadata("design:returntype", void 0)
], C.prototype, "sum", null);
__decorate([
    configurable(false),
    __metadata("design:type", Object),
    __metadata("design:paramtypes", [])
], C.prototype, "x", null);
C = __decorate([
    decorateClass('decorateClass'),
    __metadata("design:paramtypes", [])
], C);
console.log(new C().sum(1, 2)); // 3

【算法系列 - LeetCode】给定一个没有重复数字的序列,返回其所有可能的全排列。

题目

给定一个没有重复数字的序列,返回其所有可能的全排列。
示例:

输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]

思路

  1. 传入目标数组arr,定义返回值result=[],第三方遍历tmp=[]
  2. 执行函数get(arr,tmp,result)
  3. tmp数组长度与目标数组arr一致时,将tmp添加到result中,否则,对目标数组arr进行遍历,如果tmp中不存在arr[index],则将arr[index]添加到tmp中
  4. 之后使用get(arr,tmp,result)进行递归,递归时需要使用tmp.pop()来保证每轮递归中的tmp长度不变,发生变化的只是tmp的下标值
  5. 传递tmp参数需要进行deepcopy,这样下一轮的tmp才会保留本轮添加的arr[index],否则返回值为[[],[],[],[],...]

JS实现

function main(arr){
	var result = []
	get(arr,[],result)
	return result
}

function get(arr,tmp,result){
	if(tmp.length === arr.length)     //tmp与参数的数组长度相同时,push到result中
	{
		result.push(tmp)
		return
	}
	for(var index in arr)
	{
		if(!tmp.includes(arr[index]))
		{
			tmp.push(arr[index])
			get(arr,JSON.parse(JSON.stringify(tmp)),result)    //tmp需要进行深deepclone,否则会出现子数组长度为0
			tmp.pop()
		}
	}
}

Python实现

#coding=utf-8
import copy

def get(arr,tmp,result):
    if len(tmp) == len(arr):
        result.append(tmp)
        return
    for item in arr:
        if item not in tmp:
            tmp.append(item)
            # tmp1 = tmp
            get(arr,copy.deepcopy(tmp),result)
            tmp.pop()


def main(arr):
    result = list()
    get(arr,[],result)
    return result

if __name__ == "__main__":
    print(main([1,2,3]))

【python】使用openpyxl进行excel操作

openpyxl模块用来读写Excel文件。openpyxl工作时,在内存中创建Excel工作簿和工作表,然后在工作表中的单元格中进行各种数据编辑和样式编辑操作,或在工作表中绘制图形,最后再保存文件写入到Excel中。

官方文档: http://openpyxl.readthedocs.io/en/default/

1. 基本操作

1.1. 引入openpyxl库

import openpyxl

1.2. 打开工作簿(xlsx)文件

1.2.1. 全新创建工作簿

wb = openpyxl.Workbook()

得到一个wb对象,它就是我们要操作的工作簿文件的对象。

>>> wb
<openpyxl.workbook.workbook.Workbook object at 0x0000000003543358>

1.2.2. 打开现有xlsx文件:

wb = openpyxl.load_workbook(“test.xlsx”)

1.3. 保存wb对象到xlsx文件

wb.save("test.xlsx")

文件名支持路径。

以上语句获得的一个空的工作簿,默认含有一个名为“Sheet”的工作表页面。

1.4. 工作表(sheet)操作

wb对象既是整个xlsx文件在内存中的存在形式,它一般由一个或多个sheet页面组成,我们对xlsx文件的操作,主要就是在sheet页面上操作;我们在Excel软件中,能对sheet页面进行什么操作,基本使用openpyxl也都能完成。

1.4.1. 工作表的创建和选定

wb对象创建后,默认含有一个默认的名为 Sheet 的 页面,可以使用active来得到它

>>> ws1 = wb.active
>>> ws1
<Worksheet "Sheet">

或使用名称来得到它:

ws1 = wb.get_sheet_by_name('Sheet')  

ws1 = wb["Sheet"]

名称有中文要使用unicode

也可以使用工作表序号获得该页面: ws1 = wb.worksheets[0]

注:序号从0开始

可以使用 wb.create_sheet() 来创建新的sheet页面。 创建时可以同时关联一个变量。

ws2 = wb.create_sheet()

如:

import openpyxl
wb = openpyxl.Workbook()
# ws1 = wb.active
ws1 = wb.get_sheet_by_name('Sheet')
ws2 = wb.create_sheet()
wb.create_sheet()
wb.create_sheet()
wb.create_sheet()
ws1['A1'] = "This is a test!"
wb.save("test.xlsx")

对于已有的工作表,我们也可以通过遍历名称、序号等方式拿到相应的对象地址。

wb.sheetnames 是所有工作表sheet页面的名称列表。

1.4.2. 工作表命名

工作表sheet可以在创建时候直接指定名字,中文要使用unicode,也可以通过修改对象的title值,来修改名称。

import openpyxl
wb = openpyxl.Workbook()
# ws1 = wb.active
ws1 = wb.get_sheet_by_name('Sheet')
ws1.title = "Test"
ws2 = wb.create_sheet("ABC")
wb.create_sheet(u"中文表名")
ws1['A1'] = "哇嘎嘎"
wb.save("test.xlsx")
>>> print wb.sheetnames
[u'Test', u'ABC', u'\u4e2d\u6587\u8868\u540d']

1.4.3. 工作表的标签颜色修改

修改工作表的标签的颜色,等价于工作表标签右键菜单中“工作表标签颜色”功能:

ws1.sheet_properties.tabColor="0000FF"

颜色的值为颜色的RRGGBB格式。

import openpyxl
wb = openpyxl.Workbook()
#ws1 = wb.active
ws1 = wb.get_sheet_by_name('Sheet')
ws1.title = "Test"
ws2 = wb.create_sheet("ABC")
wb.create_sheet(u"中文表名")
ws1.sheet_properties.tabColor="0000FF"
ws2.sheet_properties.tabColor="FF00FF"
ws1['A1'] = "哇嘎嘎"
ws1['B2'] = "嗯~ o(* ̄▽ ̄*)o"
wb.save("test.xlsx")

1.4.4. 删除工作表

使用wb.remove_sheet()方法删除工作表

wb.remove_sheet(wb.get_sheet_by_name(“中文表名”)) wb.remove_sheet(ws2)

1.5. 单元格操作

获得一个工作表对象后,就可以工作表中的单元格进行操作。

1.5.1. 向单个单元格中写入值

  • 按照Excel单元格定位的传统方法,在指定单元格写入数据,之前例子中已有体现:
     ws1['A1'] = "哇嘎嘎"
     ws1['B2'] = "嗯~ o(* ̄▽ ̄*)o"
    
  • 在指定行、列的单元格中写入数据: ws1.cell(row=3, column=2).value = "AAAAA"

1.5.2. 遍历方式向多个单元格中写入值:

有多种的方式可对多个单元格进行选定:

  • 采用iter_cols方法,指定行列范围,对范围内的列进行遍历操作,然后再对列中的单元格操作;
  • 对应有iter_rows方法,与inter_cols区别,是遍历出所有行先;
     for col in ws1.iter_cols(min_row=2, min_col=2, max_row=7, max_col=6):
         for cell in col:
    
  • 采用传统的Excel多单元格选择的标记方法,区域选择单元格,遍历与iter_rows相同,返回每行,然后再遍历行中单元格;
    for row in ws2["B2:F7"]:
        for cell in row:
    

完整示例:

import openpyxl
wb = openpyxl.Workbook()
ws1 = wb.get_sheet_by_name('Sheet')
ws1.title = "Test"
i = 0
# 遍历方式1:
ws1['A1'] = "遍历方式1"
for col in ws1.iter_cols(min_row=2, min_col=2, max_row=7, max_col=6):
    for cell in col:
        cell.value = i
        i = i + 1
ws2 = wb.create_sheet("Test2")
i = 0
# 遍历方式2:
ws2['A1'] = "遍历方式2"
for row in ws2["B2:F7"]:
    for cell in row:
        cell.value = i
        i = i + 1
wb.save("test.xlsx")

可以看出这两种遍历方式遍历的行列顺序是完全不一样的。

import openpyxl
wb = openpyxl.Workbook()
ws1 = wb.get_sheet_by_name('Sheet')
ws1.title = "Test"
i = 0
# 遍历方式1:
ws1['A1'] = "遍历方式1"
print("遍历方式1")
j = 1
for col in ws1.iter_cols(min_row=2, min_col=2, max_row=7, max_col=6):
    print("Cols No." + str(j) + ":  ", end="")
    print(col)
    j = j + 1
    for cell in col:
        cell.value = i
        i = i + 1
ws2 = wb.create_sheet("Test2")
i = 0
# 遍历方式2:
ws2['A1'] = "遍历方式2"
print("遍历方式2")
j = 1
for row in ws2["B2:F7"]:
    print("Row No." + str(j) + ":  ", end="")
    print(row)
    j = j + 1
    for cell in row:
        cell.value = i
        i = i + 1
wb.save("test.xlsx")

执行后控制台输出:

遍历方式1
Cols No.1:  (<Cell 'Test'.B2>, <Cell 'Test'.B3>, <Cell 'Test'.B4>, <Cell 'Test'.B5>, <Cell 'Test'.B6>, <Cell 'Test'.B7>)
Cols No.2:  (<Cell 'Test'.C2>, <Cell 'Test'.C3>, <Cell 'Test'.C4>, <Cell 'Test'.C5>, <Cell 'Test'.C6>, <Cell 'Test'.C7>)
Cols No.3:  (<Cell 'Test'.D2>, <Cell 'Test'.D3>, <Cell 'Test'.D4>, <Cell 'Test'.D5>, <Cell 'Test'.D6>, <Cell 'Test'.D7>)
Cols No.4:  (<Cell 'Test'.E2>, <Cell 'Test'.E3>, <Cell 'Test'.E4>, <Cell 'Test'.E5>, <Cell 'Test'.E6>, <Cell 'Test'.E7>)
Cols No.5:  (<Cell 'Test'.F2>, <Cell 'Test'.F3>, <Cell 'Test'.F4>, <Cell 'Test'.F5>, <Cell 'Test'.F6>, <Cell 'Test'.F7>)
遍历方式2
Row No.1:  (<Cell 'Test2'.B2>, <Cell 'Test2'.C2>, <Cell 'Test2'.D2>, <Cell 'Test2'.E2>, <Cell 'Test2'.F2>)
Row No.2:  (<Cell 'Test2'.B3>, <Cell 'Test2'.C3>, <Cell 'Test2'.D3>, <Cell 'Test2'.E3>, <Cell 'Test2'.F3>)
Row No.3:  (<Cell 'Test2'.B4>, <Cell 'Test2'.C4>, <Cell 'Test2'.D4>, <Cell 'Test2'.E4>, <Cell 'Test2'.F4>)
Row No.4:  (<Cell 'Test2'.B5>, <Cell 'Test2'.C5>, <Cell 'Test2'.D5>, <Cell 'Test2'.E5>, <Cell 'Test2'.F5>)
Row No.5:  (<Cell 'Test2'.B6>, <Cell 'Test2'.C6>, <Cell 'Test2'.D6>, <Cell 'Test2'.E6>, <Cell 'Test2'.F6>)
Row No.6:  (<Cell 'Test2'.B7>, <Cell 'Test2'.C7>, <Cell 'Test2'.D7>, <Cell 'Test2'.E7>, <Cell 'Test2'.F7>)

1.5.3. 按行追加数据

采用工作表的append()方法,可以向工作表中按行追加数据:

import openpyxl
wb = openpyxl.Workbook()
ws1 = wb.get_sheet_by_name('Sheet')
ws1.title = "Test"
DATA = [
    ['第一天', 123, 12, 123, 900, 231, 7],
    ['第二天', 13, 56, 3, 900, 231, 90],
    ['第三天', 216, 38, 37, 543, 55, 376],
    ['第四天', 89, 99, 88, 453, 87, 527]
]
for row in DATA:
    ws1.append(row)
wb.save("test.xlsx")

1.5.4. 加载已有文件,读取数据

已有文件:test2.xlsx

import openpyxl
wb = openpyxl.load_workbook("test2.xlsx")
ws = wb.get_sheet_by_name('Sheet')
print("数据表最大行数:" + str(ws.max_row))
print("数据表最大列数:" + str(ws.max_column))
DATA = []
# 遍历读取
for row in ws.iter_rows(min_col=1, min_row=1, max_row=ws.max_row, max_col=ws.max_column):
    ROW = []
    for cell in row:
        ROW.append(cell.value)
    DATA.append(ROW)
print("表中读取到的数据:", end="")
print(DATA)

读取指定位置

B2 = ws['B2'].value
print("B2 = " + B2)

执行结果:

数据表最大行数:2
数据表最大列数:2
表中读取到的数据:[['测试2', None], [None, 'B列2行']]
B2 = B列2

1.5.5. 使用公式

可以直接在单元格中写入公式:

import openpyxl
wb = openpyxl.Workbook()
ws1 = wb.get_sheet_by_name('Sheet')
ws1.title = "Test"
DATA = [
    ['第一天', 123, 12, 123, 900, 231, 7],
    ['第二天', 13, 56, 3, 900, 231, 90],
    ['第三天', 216, 38, 37, 543, 55, 376],
    ['第四天', 89, 99, 88, 453, 87, 527]
]
ws1['A1'] = '这是一个测试用表格'
for row in DATA:
    ws1.append(row)
ws1.append(['合计', '=sum(B2:B5)', '=sum(C2:C5)', '=sum(D2:D5)', '=sum(E2:E5)', '=sum(F2:F5)', '=sum(G2:G5)'])
wb.save("test.xlsx")

2.单元格样式设置

单元格样式的控制,依赖openpyxl.style包,其中定义有样式需要的对象,引入样式相关:

from openpyxl.styles import PatternFill, Font, Alignment, Border, Side
  • PatternFill 填充
  • Font 字体
  • Aignment 对齐
  • Border 边框
  • Side 边线

以上五个基本可满足需要

基本用法是,将单元格对象的设置的属性赋为新的与默认不同的相应对象。

比如设置一个字体对象:

2.1. 字体设置

1.5.5. 的示例中,保存文件前先设置一下标题字体:

# 样式
font = Font(size=14, bold=True, name='微软雅黑',  color="FF0000")
ws1['A1'].font = font
wb.save("test.xlsx")

2.2. 设置边框

边框Border对象,创建时可指定边框各边的Side对象,根据之前的执行结果,默认都是无边框

# 样式
font = Font(size=14, bold=True, name='微软雅黑', color="FF0000")
thin = Side(border_style="thin", color="0000FF")
border = Border(left=thin, right=thin, top=thin, bottom=thin)
ws1['A1'].font = font
ws1['A1'].border = border
for row in ws1['A2:G6']:
    for cell in row:
        cell.border = border
wb.save("test.xlsx")

2.3. 对齐设置

# 对齐
alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
ws1['A1'].alignment = alignment
  • wrap_text=True 打开自动换行

2.4. 合并单元格

采用 merge_cells 方法合并单元格

# 对齐
alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
ws1['A1'].alignment = alignment
# 合并单元格
ws1.merge_cells('A1:G1')
wb.save("test.xlsx")

2.5.填充单元格

# 填充
fill = PatternFill(patternType="solid", start_color="33CCFF")
ws1['A1'].fill = fill
for row in ws1.iter_rows(min_row=ws1.max_row, max_col=ws1.max_column):
    for cell in row:
        cell.fill = PatternFill(patternType="solid", start_color="0066FF")
        cell.font = Font(bold=True, color="FFFFFF")
        cell.alignment = Alignment(horizontal="center")
wb.save("test.xlsx")

文章转载自:http://yaoyan.me/2017/08/python-openpyxl1/

【算法系列 - 剑指Offer】替换空格

题目

请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。

js实现

function replaceSpace(str)
{
    // write code here
    return str.replace(/ /g,'%20')
}

🚀 route-resource-preload 拆分代码,减少首屏加载资源,提供优质用户体验

route-resource-preload

🚀 专注于不影响用户最佳体验的同时,提升应用首屏加载速度,灵感来自NextJS的预加载.

gif 效果图

正常懒加载.gif

nornal-load.gif

route-resource-preload 预加载.gif

preoload.gif

从上图可以发现,route-resource-preload 预加载后,视图上基本不会再出现 loading 的情况,相对于正常懒加载的 loading 时长,用户体验层面上有了较大的提升。

为什么你需要 route-resource-preload ?

  • 拆分模块按需加载,提升应用首屏加载体验.
  • 尽最大努力地去缩短动态导入组件的加载时间(可以看作是 suspense loading 组件持续时间)以提供最佳交互体验.
  • 支持自动预加载资源(JS / Component / Module-Federation / UMD / Svg / Png 等).
  • 支持手动调用预加载.
  • 完备的 typescript 支持.

route-resource-preload 与 react.lazy 有什么不同?

route-resource-preload 在兼顾组件代码分割的同时,通过支持对组件的自动预加载手动预加载,避免因为组件渲染延迟导致组件交互体验差。

route-resource-preload 与 webpack-prefetch/preload 或 loadable-component 有什么不同?

常规组件使用效果对比

正常懒加载普通组件 及 Module-Federation

image.png

route-resource-preload 预加载普通组件 及 Module-Federation

image.png

资源 正常懒加载(ms) 预加载 (ms)
普通组件 (单个资源文件) 184 1
Module-Federation 组件 (6个资源文件) 405 8

从表中可以看出,route-resource-preload 预加载显着提升了组件的加载速度,尤其是对于复杂的组件,加载速度的提升更为明显。 这说明在复杂的业务场景下,预加载可以显着提升组件加载速度和用户体验.

以上是常规组件预加载时的对比效果

模态弹窗组件使用效果对比

模态弹窗被常规的方式(如 react.lazy)进行懒加载时,由于是渲染在视图内,会自动进行资源加载,基于route-resource-preload ,可以让模态弹窗组件 visible 为 true 时,才进行资源加载,为 false 时,不自动加载以减少应用首屏 http 请求数,同时基于预加载机制,在保证页面性能的同时,不影响用户对模态弹窗的交互体验。

正常懒加载模态弹窗

image.png

route-resource-preload 预加载模态弹窗

image.png

此处有两个http请求,是因为预加载配置了两个组件。

const Modal = useMemo(()=> dynamic({
    visible, // false 时,技术组件渲染在视图内,但不自动进行资源预加载,减少首屏非必要的 http 请求
    loader: () => import('antd/es/modal'),
    loading: () => <>loading...</>,
}),[visible])

<>
    <Modal visible={visible} />
    <PreloadLink flag="/A" onClick={()=>{setVisible(true)}}>       
        PreLoad Modal
    </PreloadLink>
</>
new RouteResourcePreloadPlugin({
    modulePreloadMap: {
      "/A": ["../components/A",'antd/es/modal']
    }
})
资源 正常懒加载(ms) 预加载 (ms)
模态弹窗 (单个资源文件) 271 45

总结

route-resource-preload 的目标是Any code can be split,在不影响用户交互体验的同时,尽可能的提升应用性能`。🚀 🚀 🚀

希望 route-resource-preload 能对你的项目有所帮助,后续还将持续探索 vite 中的使用以及对 vue 的支持,如果你有好的想法,请发表在此

在线体验

【算法系列 - LeetCode】带重复元素的排列

题目

给出一个具有重复数字的列表,找出列表所有不同的排列。

样例
样例 1:

输入:[1,1]
输出:
[
[1,1]
]
样例 2:

输入:[1,2,2]
输出:
[
[1,2,2],
[2,1,2],
[2,2,1]
]
挑战
使用递归和非递归分别完成该题。

【算法系列 - 剑指Offer】二维数组中的查找

题目

在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

思路

对数组list进行遍历,再对sublist进行二次遍历,因为是有序排列,如果当前sublist[index]比目标值val大,说明当前sublist[index]之后,所有值都比val大,可进行continue,比较下一个sublist

实现

	var list = [[1,3],[2,5],[4,8],[6,9],[10,10]]

	function main(list,val){
		for(var sublist of list)
		{
			for(var index in sublist )
			{
				if(sublist[index] === val)
				{
					return true
				}
				else if(sublist[index]>val)
				{
					continue
				}
			}
		}
		return false
	}

	console.log(main(list,10))

【Front-end State Management VS】React state management tools comparison,react hooks context / redux / mobx / zustand / jotai / valtio vs each other

React state management solution

Introduce

Scheme comparison
Frame Principle Advantage Disadvantage
hooks context Based on react hooks, developers can implement internal/external storage 1. Simple to use
2. No need to refer to third-party libraries, the smallest size
3. Support global state storage, but in complex applications Not recommended in
4. Does not depend on the react context, can be called outside the component (under the condition of external storage)
1. When the context value changes, all components that use this context will be re-rendered, based on content maintenance The more modules there are, the larger the area of influence.
2. Using a dependent provider, modifying the store cannot trigger rendering at the top level of the application (App.tsx level)
3. Constrained by the ui framework (react)
4. Dependent hooks call
react-redux Flux thinking, publish-subscribe mode, functional programming compliance, external storage 1. Does not depend on the react context, can be called outside the component
2. Supports storing global state
3. Not affected by ui Framework Constraints
1. Mental models take some time to understand, especially if you are not familiar with functional programming
2. Coding is cumbersome
mobx Observer mode + data cutoff, external storage 1. Simple to use
2. Does not depend on the react context, can be called outside the component
3. Supports storing global state
4. Not affected ui framework constraints
1. Variable state model, which may affect debugging in some cases 2. In addition to the relatively large size, the author currently does not feel any obvious shortcomings, 3.99M
zustand Flux idea, observer mode, external storage 1. Lightweight, easy to use
2. Does not depend on the react context, can be called outside the component
3. Supports storage of global state
1. The framework itself Computed attributes are not supported, but computed can be implemented indirectly with a small amount of code based on the middleware mechanism, or based on the third-party library zustand-computed
2. Constrained by the ui framework (react / vue)
jotai Based on react hook, internal storage 1. Simple to use
2. In the case of finer component granularity, jotai has better performance
3. Supports storing global state, but it is not recommended in complex applications
1. Rely on the react context and cannot be called outside the component. Relatively speaking, zustand can work better outside the react environment and globally
2. Constrained by the ui framework (react)
valtio Based on data hijacking, external storage 1. Simple to use, mobx-like (vue-like) programming experience
2. Supports storage of global state
3. Does not depend on the react context, can be called outside the component
br/> 4. Not constrained by the ui framework
1. Variable state model, which may affect debugging in some cases
2. At present, the author has not found any other particularly serious shortcomings, and I personally guess that the reason why stars are relatively less than zustand , because there is a conflict between valtio's two-way data binding idea and react.

Source

import React from "react"
import ReactDOM from "react-dom"
import { makeAutoObservable } from "mobx"
import { observer } from "mobx-react"

// Build status and related events
class Timer {
    secondsPassed = 0

    constructor() {
        makeAutoObservable(this)
    }

    increase() {
        this.secondsPassed += 1
    }

    reset() {
        this.secondsPassed = 0
    }
}

const myTimer = new Timer()

// Build a observable component
const TimerView = observer(({ timer }) => (
    <button onClick={() => timer.reset()}>Seconds passed: {timer.secondsPassed}</button>
))

ReactDOM.render(<TimerView timer={myTimer} />, document.body)

// Trigger update event
setInterval(() => {
    myTimer.increase()
}, 1000)
  • zustand
import { create } from 'zustand'

// Build status and related events
const useBearStore = create((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}))

// Render component
function BearCounter() {
  const bears = useBearStore((state) => state.bears)
  return <h1>{bears} around here ...</h1>
}

// Trigger update event
function Controls() {
  const increasePopulation = useBearStore((state) => state.increasePopulation)
  return <button onClick={increasePopulation}>one up</button>
}
  • jotai
import { atom } from 'jotai'

const countAtom = atom(0)

function Counter() {
  // Build status and related events
  const [count, setCount] = useAtom(countAtom)
  return (
    <h1>
      {count}
      <button onClick={() => setCount(c => c + 1)}>one up</button>
    </h1>
  )
}
  • valtio
import { proxy, useSnapshot } from 'valtio'

const state = proxy({ count: 0, text: 'hello' })

function Counter() {
  const snap = useSnapshot(state)
  return (
    <div>
      {snap.count}
      <button onClick={() => ++state.count}>+1</button>
    </div>
  )

related advice

  1. If you need an alternative to useState+useContext, jotai is very suitable, that is, atomic component state management or state sharing between a small number of components.
  2. If you are used to redux or like the natural immutable update of react, then zustand will be very suitable.
  3. If you are used to vue/slute/mobx, or new to JS/React, the variable model of valtio will be very suitable.
  4. If you are using zustand (immutable data model such as redux/) + immer, it is recommended to use valtio(mobx)
  5. mobx has the concept of actions, and the concept of valtio is simpler (free). If you want the project to be more standard, you can use mobx. If you want the project to be more free and convenient, you can use valtio

If this article is helpful to you, please give me a 👍
The next issue will bring an analysis of the advantages and disadvantages of Vue state management tools, welcome to pay attention to myBlog](https://github.com/AwesomeDevin/blog) 🌟

【Javascript】深入理解javascript作用域与作用域链

作用域链(Scope Chain)

在JavaScript中,函数也是对象,实际上,JavaScript里一切都是对象。函数对象和其它对象一样,拥有可以通过代码访问的属性和一系列仅供JavaScript引擎访问的内部属性。其中一个内部属性是[[Scope]],该内部属性包含了函数被创建的作用域中对象的集合,这个集合被称为函数的作用域链,它决定了哪些数据能被函数访问。
在函数add创建时,它的作用域链中会填入一个全局对象,该全局对象包含了所有全局变量,函数add的作用域将会在执行时用到

function add(num1,num2) {
    var sum = num1 + num2;
    return sum;
}

执行此函数时会创建一个称为“运行期上下文(execution context)”的内部对象,运行期上下文定义了函数执行时的环境。每个运行期上下文都有自己的作用域链,用于标识符解析,当运行期上下文被创建时,而它的作用域链初始化为当前运行函数的[[Scope]]所包含的对象。

这些值按照它们出现在函数中的顺序被复制到运行期上下文的作用域链中。它们共同组成了一个新的对象,叫“活动对象(activation object)”,该对象包含了函数的所有局部变量、命名参数、参数集合以及this,然后此对象会被推入作用域链的前端,当运行期上下文被销毁,活动对象也随之销毁。

在函数执行过程中,每遇到一个变量,都会经历一次标识符解析过程以决定从哪里获取和存储数据。该过程从作用域链头部,也就是从活动对象开始搜索,查找同名的标识符,如果找到了就使用这个标识符对应的变量,如果没找到继续搜索作用域链中的下一个对象,如果搜索完所有对象都未找到,则认为该标识符未定义。函数执行过程中,每个标识符都要经历这样的搜索过程。

作用域链和代码优化

从作用域链的结构可以看出,在运行期上下文的作用域链中,标识符所在的位置越深,读写速度就会越慢。如上图所示,因为全局变量总是存在于运行期上下文作用域链的最末端,因此在标识符解析的时候,查找全局变量是最慢的。所以,在编写代码的时候应尽量少使用全局变量,尽可能使用局部变量。一个好的经验法则是:如果一个跨作用域的对象被引用了一次以上,则先把它存储到局部变量里再使用。例如下面的代码:

function changeColor(){
    document.getElementById("btnChange").onclick=function(){
        document.getElementById("targetCanvas").style.backgroundColor="red";
    };
}

这个函数引用了两次全局变量document,查找该变量必须遍历整个作用域链,直到最后在全局对象中才能找到。这段代码可以重写如下:

function changeColor(){
    var doc=document;
    doc.getElementById("btnChange").onclick=function(){
        doc.getElementById("targetCanvas").style.backgroundColor="red";
    };
}

闭包

那作用域链地作用仅仅只是为了标识符解析吗? 再来看一段代码

<script>
  function outer(){
     var scope = "outer";
     function inner(){
        return scope;
     }
     return inner;
  }
  var fn = outer();
  fn();
</script>

outer()内部返回了一个inner函数,当调用outer时,inner函数的作用域链就已经被初始化了(复制父函数的作用域链,再在前端插入自己的活动对象)

一般来说,当某个环境中的所有代码执行完毕后,该环境被销毁(弹出环境栈),保存在其中的所有变量和函数也随之销毁(全局执行环境变量直到应用程序退出,如网页关闭才会被销毁)

但是像上面那种有内部函数的又有所不同,当outer()函数执行结束,执行环境被销毁,但是其关联的活动对象并没有随之销毁,而是一直存在于内存中,因为该活动对象被其内部函数的作用域链所引用。

像上面这种内部函数的作用域链仍然保持着对父函数活动对象的引用,就是闭包(closure)

改变作用域链

函数每次执行时对应的运行期上下文都是独一无二的,所以多次调用同一个函数就会导致创建多个运行期上下文,当函数执行完毕,执行上下文会被销毁。每一个运行期上下文都和一个作用域链关联。一般情况下,在运行期上下文运行的过程中,其作用域链只会被 with 语句和 catch 语句影响。
with语句是对象的快捷应用方式,用来避免书写重复代码。例如:

function initUI(){
    with(document){
        var bd=body,
            links=getElementsByTagName("a"),
            i=0,
            len=links.length;
        while(i < len){
            update(links[i++]);
        }
        getElementById("btnInit").onclick=function(){
            doSomething();
        };
    }
}

这里使用width语句来避免多次书写document,看上去更高效,实际上产生了性能问题。

当代码运行到with语句时,运行期上下文的作用域链临时被改变了。一个新的可变对象被创建,它包含了参数指定的对象的所有属性。这个对象将被推入作用域链的头部,这意味着函数的所有局部变量现在处于第二个作用域链对象中,因此访问代价更高了

这里使用width语句来避免多次书写document,看上去更高效,实际上产生了性能问题。

另外一个会改变作用域链的是try-catch语句中的catch语句。当try代码块中发生错误时,执行过程会跳转到catch语句,然后把异常对象推入一个可变对象并置于作用域的头部。在catch代码块内部,函数的所有局部变量将会被放在第二个作用域链对象中
  

try{
    doSomething();
}catch(ex){
    alert(ex.message); //作用域链在此处改变
}

请注意,一旦catch语句执行完毕,作用域链机会返回到之前的状态。try-catch语句在代码调试和异常处理中非常有用,因此不建议完全避免。你可以通过优化代码来减少catch语句对性能的影响。一个很好的模式是将错误委托给一个函数处理,例如:

try{
    doSomething();
}catch(ex){
    handleError(ex); //委托给处理器方法
}

优化后的代码,handleError方法是catch子句中唯一执行的代码。该函数接收异常对象作为参数,这样你可以更加灵活和统一的处理错误。由于只执行一条语句,且没有局部变量的访问,作用域链的临时改变就不会影响代码性能了。

结语

作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问
作用域链前端,都是当前执行环境的代码所在环境的变量对象。

【Javascript】手把手教你自定义一个markdown编辑器

引言

markdown 是一种创作者通过输入简洁的文字便能够实现 自动排版 的排版语法,目前国内包括知乎、简书、掘金、v2ex,国外包括 stack overflow、github, 等各大内容创作平台都已经支持了 markdown 语法(当然也包括 jelly ),创作者也因为习惯了 markdown 语法,如果某个平台不支持 markdown 语法,那么必然会留下一个不好的印象,甚至可能被拉黑名单。
总而言之,拥有一个支持 markdown 语法的编辑器对于内容创作平台而言是尤为重要的。

那么,作为一个开发者来说,我们应该如何实现一个 markdown 编辑器呢?

主要做的事情有两部分:
1. 针对文本内容进行语法分析并自定义样式
2. 针对代码关键词需要高亮展示

一. 针对文本内容进行语法分析

1.1 markdown语法转html标签

就 markdown 语法而言,我们获取的输入值往往是:

# 这是标题
> 这是内容
#### 这是表格
表头1 | 表头2 | 表头3
------|-------|-------
内容1 | 内容2 | 内容3

这些输入值保存到数据库中到文本内容为:

# 这是标题↵> 这是内容↵#### 这是表格↵表头1 | 表头2 | 表头3↵------|-------|-------↵内容1 | 内容2 | 内容3

这里我们需要将对应markdown转变为浏览器可以识别的html标签:

  • # 文字处理为<h1>文字</h1>标签
  • > 文字处理为<blockquote><p>文字<p></blockquote>标签
  • xxx | xxx | xxx \n ---|---|---\n内容1 | 内容2 | 内容3处理为<table><thead><tbody><th><td>标签

这里推荐使用第三方库showdown,它是一个 JavascriptHTML的转换器,可以在客户端(在浏览器中)或服务器端(NodeJs )使用。

需要注意的是 showdown 无法识别特殊符号,在调用makeHtml()前,需要先将特殊符号处理为\n换行符。

text.replace(//g,'\n')

具体代码如下:

import showdown from 'showdown'
const converter = new showdown.Converter()
converter.setFlavor('github')
converter.makeHtml('# 这是标题↵> 这是内容↵#### 这是表格↵表头1 | 表头2 | 表头3↵------|-------|-------↵内容1 | 内容2 | 内容3'.replace(//g,'\n'))

最终得到 HTML 结构:

<h1 id="这是标题">这是标题</h1>
<blockquote>
  <p>这是内容</p>
</blockquote>
<h4 id="这是表格">这是表格</h4>
<table>
  <thead>
    <tr>
      <th id="表头1">表头1</th>
      <th id="表头2">表头2</th>
      <th id="表头3">表头3</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>内容1</td>
      <td>内容2</td>
      <td>内容3</td>
    </tr>
  </tbody>
</table>
1.2 markdown语法自定义样式

使用标签选择器自定义样式

 blockquote {
    color: #7f8fa4;
    font-size: inherit;
    padding: 8px 24px;
    margin: 16px 0;
    border-left: 3px solid #eaeaea;
    p {
      margin: 0;
    }
  }
  table {
    thead {
      background: #292c34;
      color: #a9b0bd;
      th {
        border: 1px solid #292c34;
        border-right-color: #a9b0bd;
        padding: 10px 20px;
        box-sizing: border-box;
        &:nth-last-child(1) {
          border-color: #292c34;
        }
      }
    }
    tbody {
      td {
        border: 1px solid #292c34;
        padding: 10px 20px;
        box-sizing: border-box;
      }
    }
  }

实际效果:

二. 针对代码关键词需要高亮展示

2.1 提取代码块关键词

我们使用 markdown 语法展示代码时,往往会带上语言类型,如javascript:

转换效果如下:

<pre>
  <code class="javascript language-javascript">
    import showdown from 'showdown'
    const converter = new showdown.Converter()
    converter.setFlavor('github')
    converter.makeHtml('# 这是标题
        **这是加粗的文字**
        *这是斜体*'.replace(/
    /g,'\n'))</code>
</pre>

转换结果使用<pre><code>标签将整个代码块都给包裹在里面,并不能够区分代码块关键词(eg: 'import'、'class'),无法满足我们的需求。 这时便需要借助highlight.js来提取关键词,同时给对应关键词添加上className

2.2 highlight.js

highlight.js是一个 javascript 编写的语法高亮器,可以同时运行在浏览器跟服务端,不依赖任何框架,且具备自动检测语言功能。
highlightAll()方法可以自动识别页面上的

块,并自动检测语言。
识别结果:

&lt;pre&gt;&lt;code 
<span class="hljs-keyword">class</span>
=&quot;javascript language-javascript&quot;&gt;
<span class="hljs-keyword">import</span> 
showdown 
<span class="hljs-keyword">from</span> 
<span class="hljs-string">&#x27;showdown&#x27;</span>
const converter = 
<span class="hljs-built_in">new</span> 
showdown.Converter()
converter.setFlavor(
<span class="hljs-string">&#x27;github&#x27;</span>
)
converter.makeHtml(
<span class="hljs-string">&#x27;# 这是标题
**这是加粗的文字**
*这是斜体*&#x27;
</span>.replace(/
/g,
<span class="hljs-string">&#x27;\n&#x27;</span>))&lt;/code&gt;&lt;/pre&gt;

该库支持的语言种类繁多,包括gopythonjava等,详情可查看:
https://github.com/highlightjs/highlight.js/blob/main/SUPPORTED_LANGUAGES.md

2.3 代码高亮

代码高亮我们可以通过css自定义样式,也可以尝试引入hightlighth.js提供的官方样式(vscodeatomxcodegithub等)。

@import '../node_modules/highlight.js/styles/atom-one-dark.css';
2.4 代码高亮效果

【Javascript】深入理解async/await的实现,Generator+Promise = Async/Await

概念

Generator 函数是 ES6 提供的一种异步编程解决方案,执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

yield表达式

形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态。

function* foo(x) {
    yield 1
    yield 2
    yield 3
    yield 4
    return 5
}

必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。

function* foo() {
    yield 1
    yield 2
    return 3
}

var f = foo()
f.next()     //{ value: 1, done: false }
f.next().value     //2
f.next()     //{ value: 3, done: true}
f.next()     //{ value: undefined, done: true}

Generator 函数已经运行完毕,next方法返回对象的value属性为3,done属性为true,之后再执行next(),done都为true,value未undefined

next方法的参数

yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。

function* foo(x) {
  var y = 2 * (yield (x + 1));
  var z = yield (y / 3);
  return (x + y + z);
}

var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}

var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }

Generator + Promise = 强大的异步回调方式

没有回调
function sayhello() {
    setTimeout(()=>{
        console.log(123)
    },3000)
}

function helloworld() {
    const data =  sayhello();
    console.log(data);
    console.log(456)
}
helloworld()

这里写图片描述

使用async/await

function sayhello(){
	return new Promise((resolve)=>{
		setTimeout(()=>{
			resolve(123)
	        console.log(123)
	    },3000)
	})
}

async helloworld(){
    const data = await sayhello()
    console.log(data)
    console.log(456)
}
使用Generator + Promise 实现async/await
function co(gen) {
    if(!gen) return
	return new Promise((resolve,reject)=>{
		var it = gen();
		try{
			function step(next){
				if(next.done)
				{
					return resolve(next.value)
				}
				else{
					Promise.resolve(next.value).then((res)=>{
						return step(it.next(res))
					},(e)=>{
						return step(it.throw(e))
					})
				}
			}
			step(it.next())
		}
		catch(e){
			return reject(e)
		}
	})
}

function sayhello(){
       // 之所以需要返回一个 promise ,是因为 co 函数会在底层使用 promise 同步执行 sayhello ,为了保证 sayhello 同步执行完该异步函数后获取到正确的返回值,需要使用promise包裹返回值。
	return new Promise((resolve)=>{
		setTimeout(()=>{
			resolve(123)
	        console.log(123)
	    },3000)
	})
}

co(
	function* helloworld(){
		const data = yield sayhello()
		console.log(data)
		console.log(456)
	}
)

这里写图片描述

可以看到,通过Generator + Promise(async/await)我们已经拿到了延时器中的数据。

任何复杂的异步功能都可以被promise搞定,而且你还可以用generator把这些流程写的像同步代码一样。只要你让yield返回一个promise。

【算法系列 - 剑指Offer】矩形覆盖

题目描述

我们可以用21的小矩形横着或者竖着去覆盖更大的矩形。请问用n个21的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法?

JS实现

function rectCover(number)
{
    // write code here
    if(number<=2){
        return number
    }else{
        var arr = []
        arr[1] = 1
        arr[2] = 2
        for(var i = 3;i<=number;i++)
        {
            arr[i] = arr[i-1] + arr[i-2]
        }
    }
    return arr[number]
}

【前端状态管理】React状态管理工具优劣势分析 react hooks context / redux / mobx / zustand / jotai / recoil / valtio

什么是状态管理?

“状态”是描述应用程序当前行为的任何数据。这可能包括诸如“从服务器获取的对象列表”、“当前选择的项目”、“当前登录用户的名称”和“此模式是否打开?”等值。

众所周知,我们在研发一个复杂应用的过程中,一套好的状态管理方案是必不可少的,既能提升研发效率,又能降低研发维护成本,那么状态管理方案那么多,它们有什么不同,我们又该如何选择适合当前应用的方案呢?

本期将主要就 react 的常用状态管理方案进行对比分析,希望对各位看客有帮助。

React 状态管理方案

方案介绍
方案对比
框架 原理 优点 缺点
hooks context 基于 react hook,开发者可实现内/外部存储 1. 使用简单
2. 不需要引用第三方库,体积最小
3. 支持存储全局状态,但在复杂应用中不推荐
4. 不依赖 react 上下文,可在组件外调用(外部存储的条件下)
1. context value发生变化时,所有用到这个context的组件都会被重新渲染,基于 content 维护的模块越多,影响范围越大。
2.依赖 Context Provider 包裹你的应用程序,修改 store 无法在应用最顶层(App.tsx 层级)触发渲染
3. 受ui框架约束(react)
4. 依赖hooks调用
react-redux Flux**,发布订阅模式,遵从函数式编程,外部存储 1. 不依赖 react 上下文,可在组件外调用
2. 支持存储全局状态
3. redux 本身是一种通用的状态解决方案
1. 心智模型需要一些时间来理解,特别是当你不熟悉函数式编程的时候
2. 依赖 Context Provider 包裹你的应用程序,修改 store 无法在应用最顶层(App.tsx 层级)触发渲染
3.受 ui 框架约束(react)
mobx 观察者模式 + 数据截止,外部存储 1. 使用简单,上手门槛低
2. 不依赖 react 上下文,可在组件外调用
3. 支持存储全局状态
4.通用的状态解决方案
1.可变状态模型,某些情况下可能影响调试
2. 除了体积相对较大之外,笔者目前未感觉到较为明显的缺点,3.99M
zustand Flux**,观察者模式,外部存储 1. 轻量,使用简单,上手门槛低
2. 不依赖 react 上下文,可在组件外调用
3. 支持存储全局状态
4. 通用的状态解决方案
1.框架本身不支持 computed 属性,但可基于 middleware 机制通过少量代码间接实现 computed ,或基于第三方库 zustand-computed 实现
jotai 基于 react hook,内部存储 1. 使用简单
2. 组件颗粒度较细的情况下,jotai性能更好
3.支持存储全局状态
1. 依赖 react 上下文, 无法组件外调用,相对而言, zustand 在 react 环境外及全局可以更好地工作
2.受ui框架约束(react)
recoil 进阶版 jotai,基于 react hook + provider context,内部存储 相对于 jotai而言,会更重一些,但**基本不变,拥有一些 jotai 未支持的特性及 api,如:
1.监听 store 变化
2. 针对 atom 的操作拥有更多的 api,编程上拥有更多的可能性,更加有趣
拥有 jotai 所有的缺点,且相对于 jotai 而言:
1.使用 recoil 需要 < RecoilRoot > 包裹应用程序
2. 编写 selector 会复杂一些
valtio 基于数据劫持,外部存储 1. 使用简单,类mobx(类vue)的编程体验
2.支持存储全局状态
3.不依赖 react 上下文,可在组件外调用
4. 通用的状态解决方案
1.可变状态模型,某些情况下可能影响调试
2.目前笔者没发现其它特别大的缺点,个人猜测之所以star相对zustand较少,是因为 valtio 的数据双向绑定**与 react 存在冲突。

Source

import React from "react"
import ReactDOM from "react-dom"
import { makeAutoObservable } from "mobx"
import { observer } from "mobx-react"

// 状态及相关事件
class Timer {
    secondsPassed = 0

    constructor() {
        makeAutoObservable(this)
    }

    increase() {
        this.secondsPassed += 1
    }

    reset() {
        this.secondsPassed = 0
    }
}

const myTimer = new Timer()

// 构建可观擦组件
const TimerView = observer(({ timer }) => (
    <button onClick={() => timer.reset()}>Seconds passed: {timer.secondsPassed}</button>
))

ReactDOM.render(<TimerView timer={myTimer} />, document.body)

// 触发更新事件
setInterval(() => {
    myTimer.increase()
}, 1000)
  • zustand
import { create } from 'zustand'

// 状态及相关事件
const useBearStore = create((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}))

// 渲染视图
function BearCounter() {
  const bears = useBearStore((state) => state.bears)
  return <h1>{bears} around here ...</h1>
}

// 触发更新事件
function Controls() {
  const increasePopulation = useBearStore((state) => state.increasePopulation)
  return <button onClick={increasePopulation}>one up</button>
}
  • jotai
import { atom } from 'jotai'

const countAtom = atom(0)

function Counter() {
  // 状态及相关事件
  const [count, setCount] = useAtom(countAtom)
  return (
    <h1>
      {count}
      <button onClick={() => setCount(c => c + 1)}>one up</button>
    </h1>
  )
}
  • recoil
const fontSizeState = atom({  
  key: 'fontSizeState',  
  default: 14,  
});
function FontButton() {  
  const [fontSize, setFontSize] = useRecoilState(fontSizeState);  
  return (  
    <button onClick={() => setFontSize((size) => size + 1)} style={{fontSize}}>  
      Click to Enlarge  
    </button>  
  );  
}
  • valtio
import { proxy, useSnapshot } from 'valtio'

const state = proxy({ count: 0, text: 'hello' })

function Counter() {
  const snap = useSnapshot(state)
  return (
    <div>
      {snap.count}
      <button onClick={() => ++state.count}>+1</button>
    </div>
  )

相关建议

  1. 如果你需要useState+useContext的替代品,那么jotai非常适合,即原子化的组件状态管理或少量组件间状态共享。
  2. 如果你习惯了redux或喜欢react的自然不可变更新,那么zustand将非常适合。
  3. 如果你习惯了vue/ slute /mobx,或者是JS/React的新手,valtio的可变模型将很适合。
  4. 如果你在使用 zustand(redux/等不可变数据模型) + immer,建议改用valtio(mobx)
  5. mobx有actions概念,而valtio概念更为简单(自由),如果你希望工程更为规范,可以使用mobx,如果是希望工程更为自由便捷,可以使用valtio
  6. recoiljotai的编程**类似,但提供了更多的 api 与 特性,针对原子状态拥有更多的可操作性,同时包体积也更大,但由于recoil功能庞大,其使用相对于jotai会繁琐一些,如果你希望工程轻巧便捷可以选用jotai,如果你想试试原子状态更多的可能性,那么试试recoil吧。

如果该文章对你有帮助,请给我点个👍吧~
下期将带来Vue状态管理工具优劣势分析, 欢迎关注我的Blog 🌟

【算法系列 - LeetCode】最小路径和

题目

给定一个只含非负整数的m*n网格,找到一条从左上角到右下角的可以使数字和最小的路径。

输入: [[1,3,1],[1,5,1],[4,2,1]]
输出:7
路线为: 1 -> 3 -> 1 -> 1 -> 1。

JS实现

function fn(arr){
	for(var i in arr)
	{
		for(var j in arr[0]){
			if(i==0&&j>0)
			{
				arr[i][j] += arr[i][j-1]
			}
			else if(j==0&&i>0)
			{
				arr[i][j] += arr[i-1][j]
			}
			else if(i>0&&j>0)
			{
				arr[i][j] += Math.min(arr[i][j-1],arr[i-1][j])
			}
		}
	}
	return arr[arr.length-1][arr[0].length-1]
}

【微信小程序】微信小程序保存二维码海报到相册,以及解决二维码不够清晰的问题

小程序保存二维码海报

  • 添加 canvas 元素poster
  <canvas class="poster" canvas-id="poster" style="width:300rpx;height:300rpx;"></canvas>
  • 给poster添加海报、二维码
initCanvas(){
  var ctx = wx.createCanvasContext('poster')
  ctx.drawImage('https://image.watsons.com.cn//upload/0851366c.png', 0, 0, 241, 368)   //画海报
  ctx.drawImage(qrCodeUrl, 70, 240, 120,120) //画二维码
  ctx.draw()
  this.save()  //生成微信临时模板文件path
}
  • 生成微信临时模板文件path
save(){  
  var self = this;
  setTimeout(()=>{
  wx.canvasToTempFilePath({
		  x: 0,
		  y: 0,
		  width: 241,
		  height: 368,
		  destWidth: 241,
		  destHeight: 368,
		  canvasId: 'poster',
		  success: function(res) {
		    console.log('save',res.tempFilePath)
		    self.saveUrl = res.tempFilePath  //保存临时模板文件路径
		  },
		  fail:function(res){
		   // wx.showToast({
		   // 	title:'网络繁忙',
		   // 	icon:'none'
		   // })
		   return
		  }
  })
  },500)
}
  • 保存到手机相册 wx.downloadFile 可以不要,因为此时saveUrl已经是wx.downloadFile,如果是第三方路径则需要先downloadFile
saveImageToPhotosAlbum(){
  console.log(this.saveUrl)
  wx.showLoading({
	  title:'下载中...'
  })
  var self = this;
  wx.downloadFile({
      url: self.saveUrl,
      success: function(res) {
          // 只要服务器有响应数据,就会把响应内容写入文件并进入 success 回调,业务需要自行判断是否下载到了想要的内容
          if (res.statusCode === 200) {
			          wx.saveImageToPhotosAlbum({
				           filePath:res.tempFilePath,
		          success(result) {
			          wx.hideLoading()
			          wx.showToast({
				          title:'已保存至相册',
				          icon:'none',
          
			          })
			          
		          },
		          fail(result){
			          wx.hideLoading()
			          wx.showToast({
				          title:'下载失败',
				          icon:'none',
          
			          })
			          
		          }
          })
          }
      },
      fail(result){
        wx.hideLoading()
        wx.showToast({
	        title:'下载失败',
	        icon:'none',
        })
      }
  })
  },
解决海报中的二维码不够清晰的问题
  • 添加 id 为generate的canvas,poster用作展示,generate用作保存
	<canvas class="poster" canvas-id="poster" style="width:300rpx;height:300rpx;"></canvas>
    <canvas class="generate" canvas-id="generate" style="width:1500rpx;height:2296rpx;"></canvas>
  • 在generate 上 initCanvas
var ctx = wx.createCanvasContext('generate')
ctx.drawImage('https://image.watsons.com.cn//upload/0851366c.png', 0, 0, 750, 1148)  //画海报
ctx.drawImage(QrCodeUrl, 300, 886, 150,150)  //画二维码
ctx.draw()
this.save()	
  • 生成微信临时模板文件path ,同时生成展示用的海报
save(){   //保存二维码
	var self = this;
	setTimeout(()=>{
		wx.canvasToTempFilePath({
		  x: 0,
		  y: 0,
		  width: 750,
		  height: 1148,
		  destWidth: 750,
		  destHeight: 1148,
		  canvasId: 'generate',
		  success: function(res) {
		  	this.tempFilePath = res.tempFilePath
		    console.log('save',res.tempFilePath)
		    var ctx = wx.createCanvasContext('poster')    //加载展示用的海报
		    ctx.drawImage(res.tempFilePath, 0, 0, 241, 368)
		    ctx.draw()
		    self.saveUrl = res.tempFilePath    //保存临时模板文件路径
		  },
		  fail:function(res){
		  	return
		  }
		})
	},500)
}
  • 保存到手机相册
代码跟之前描述的一样

如果觉得我的文章对你有帮助,欢迎关注我的blog

相关知识点

【Javascript】深入理解async/await的实现,Generator+Promise = Async/Await
【Javascript】深入理解this作用域问题以及new运算符对this作用域的影响
【Javascript】手写运算符new创建实例并实现js继承

【Javascript】抛弃Redux + flux**,使用 react hooks + context 进行方便快捷的全局状态管理

该方案主要分为三步:1. 基于 hooks 构建 Store 2. 将 Store 基于 context 传递给子组件 3. 子组件更新 Store,并触发渲染,该方案不适用于复杂应用

1.构建Store

userStore.tsx

import { useState } from 'react'
export default function useUserStore () {
  const [user, setUser] = useState<IUser>({  })

  return {
    setUser,
    user,
  }
}

loginStore.tsx

import { useState } from 'react'
export default function useLoginStore () {
  const [login, setLogin] = useState(false)

  return {
    login,
    setLogin,
  }
}

2.将 Store 基于 context 传递给子组件

context.tsx

import useUserStore from "./userStore";
import useLoginStore from "./loginStore";
import { createContext } from "react";

export const context = createContext(null);
export default function Context({ children }) {
  const userStore = useUserStore();
  const loginStore = useLoginStore();

  console.log("userStore", userStore);

  const contextValue = {
    userStore,
    loginStore
  };

  return <context.Provider value={contextValue}>{children}</context.Provider>;
}

app.tsx

import Context from "./context";
import Child from "./child";

export default function Index() {
  return (
    <Context>
      <Child />
    </Context>
  );
}

3.子组件更新 Store,并触发渲染,该方案不适用于复杂应用

child.tsx

import { useContext, useEffect } from "react";
import { context } from "./context";

export default function Child() {
  const store = useContext(context);
  console.log(store);
  const { user, setUser } = store?.userStore || {};
  const { login, setLogin } = store?.loginStore || {};

  useEffect(() => {
    setTimeout(() => {
      setUser({ name: "AwesomeDevin" });
      setLogin(true);
    }, 2000);
  }, [setUser, setLogin]);

  return (
    <div>
      <p>{user?.name || "未命名"}</p>
      <p>{login ? "已登陆" : "未登录"}</p>
    </div>
  );
}

在线DEMO https://codesandbox.io/s/react-context-ease-store-lv1ibp?file=/src/App.js

【javascript】解决由于参数编码问题导致服务端报500 【如:微信小程序 session_key 加密数据解密算法 报错】

我的报错场景

在【微信小程序】开发中 加密数据解密算法 报错,后来经过排查发现是由于session_key('1744eFm+jcmxMOKxMw==')中存在特殊符号'+',后端接收到的参数为'1744eFm jcmxMOKxMw==',前后不一致,导致解密报错
这里写图片描述

解决办法

前端使用encodeURIComponent(key)对key进行编码,后端decodeURIComponent(key),这样就能拿到完整的数据

顺便说一下encodeURI与encodeURIComponent的区别

encodeURI 该方法的目的是对 URI 进行完整的编码,因此对以下在 URI 中具有特殊含义的 ASCII 标点符号,encodeURI() 函数是不会进行转义的:;/?:@&=+$,#

encodeURIComponent “; / ? : @ & = + $ , #”,这些在encodeURI()中不被编码的符号,在encodeURIComponent()中统统会被编码。至于具体的编码方法,两者是一样。

通过切割代码和预加载来提高页面加载速度

预加载的好与不好

预加载意味着会发更多的请求,并且很可能用户最终也不会使用,这些流量会造成更大的服务器压力,换句话就是你会为此花更多钱。

好处就是用户体验会好点,对于网速很快的用户来说提升只是一点点,对于网速稍慢的用户来说提升会更明显。

参考资料

  1. Lazy loading (and preloading) components in React 16.6 | by Rodrigo Pombo | HackerNoon.com | Medium
  2. React Lazy: a take on preloading views - Maxime Heckel's Blog

关于 React.lazy 和动态 import 的一些测试
参考 Lazy loading (and preloading) components in React 16.6 | by Rodrigo Pombo | HackerNoon.com | Medium 做了一些测试。

不做动态 import

import { useState } from 'react'

// 这种是把 Desc 和当前页面其它代码打包到一个文件了
// 访问这个页面时就会下载这个文件然后显示界面
// 点击按钮后 Desc 会马上显示
import Desc from './Desc'

function App() {
  const [showDesc, setShowDesc] = useState(false)

  return (
    <div>
      <button onClick={() => setShowDesc(true)}>显示描述</button>
      {showDesc && <Desc />}
    </div>
  )
}

export default App

加上动态 import

import { useState, lazy, Suspense } from 'react'

// 这种是把 Desc 单独打包了,但是在点击按钮的时候才会去下载文件
// 所以会先显示 loading... 然后文件下载完后显示 Desc
const Desc = lazy(() => import('./Desc'))

function App() {
  const [showDesc, setShowDesc] = useState(false)

  return (
    <div>
      <button onClick={() => setShowDesc(true)}>显示描述</button>
      <Suspense fallback={<div>loading...</div>}>
        {showDesc && <Desc />}
      </Suspense>
    </div>
  )
}

export default App

加上动态 import 并提前下载文件

import { useState, lazy, Suspense } from 'react'

// 会在加载这个页面时就去下载 Desc 文件
// 当点击按钮的时候一般 Desc 文件已经下载完了,所以会直接显示
const descPromise = import('./Desc')
const Desc = lazy(() => descPromise)

function App() {
  const [showDesc, setShowDesc] = useState(false)

  return (
    <div>
      <button onClick={() => setShowDesc(true)}>显示描述</button>
      <Suspense fallback={<div>loading...</div>}>
        {showDesc && <Desc />}
      </Suspense>
    </div>
  )
}

export default App

加上动态 import 并在鼠标 hover 时下载文件

import { useState, lazy, Suspense } from 'react'

const importDesc = () => import('./Desc')
const Desc = lazy(importDesc)

function App() {
  const [showDesc, setShowDesc] = useState(false)

  // 在鼠标移入时再去下载 Desc
  const onMouseEnter = () => {
    importDesc()
  }

  return (
    <div>
      <button onClick={() => setShowDesc(true)} onMouseEnter={onMouseEnter}>
        显示描述
      </button>
      <Suspense fallback={<div>loading...</div>}>
        {showDesc && <Desc />}
      </Suspense>
    </div>
  )
}

export default App

封装一下,方便应用到其它组件

import { useState, lazy, Suspense } from 'react'

function lazyWithPreload(importFunc) {
  const Component = lazy(importFunc)
  // 加上一个 preload 属性,方便调用
  Component.preload = importFunc
  return Component
}

const Desc = lazyWithPreload(() => import('./Desc'))

function App() {
  const [showDesc, setShowDesc] = useState(false)

  // 在鼠标移入时再去下载 Desc
  const onMouseEnter = () => {
    Desc.preload()
  }

  return (
    <div>
      <button onClick={() => setShowDesc(true)} onMouseEnter={onMouseEnter}>
        显示描述
      </button>
      <Suspense fallback={<div>loading...</div>}>
        {showDesc && <Desc />}
      </Suspense>
    </div>
  )
}

export default App

如果动态 import 的组件 A 里面还动态 import 了其它的组件 B

这种情况的话,是会在需要展示组件 B 的时候才去下载组件 B 的代码,因为你在鼠标移上去的时候只预加载了组件 A 。

// App.js
import { useState, Suspense } from 'react'
import lazyWithPreload from './lazyWithPreload'

const Desc = lazyWithPreload(() => import('./Desc'))

function App() {
  const [showDesc, setShowDesc] = useState(false)

  // 在鼠标移入时再去下载 Desc
  const onMouseEnter = () => {
    Desc.preload()
  }

  return (
    <div>
      <button onClick={() => setShowDesc(true)} onMouseEnter={onMouseEnter}>
        显示描述
      </button>
      <Suspense fallback={<div>loading...</div>}>
        {showDesc && <Desc />}
      </Suspense>
    </div>
  )
}

export default App
// Desc.js
import { Suspense } from 'react'
import lazyWithPreload from './lazyWithPreload'

const SubDesc = lazyWithPreload(() => import('./SubDesc'))

function Desc() {
  return (
    <div>
      <div>这是一段描述,假装这是一个很复杂的组件。</div>
      {/* 这里会先显示 loading 然后再显示 SubDesc 内容 */}
      <Suspense fallback={<div>loading...</div>}>
        <SubDesc />
      </Suspense>
    </div>
  )
}

export default Desc

直接使用 loadable-components 库来做预加载

react-router 推荐的 code splitting 库是 loadable-components ,这个库是支持 预加载 功能的。

import { useState } from 'react'
import loadable from '@loadable/component'

const Desc = loadable(() => import('./Desc'), {
  fallback: <div>loading...</div>,
})

function App() {
  const [showDesc, setShowDesc] = useState(false)

  // 在鼠标移入时再去下载 Desc
  const onMouseEnter = () => {
    Desc.preload()
  }

  return (
    <div>
      <button onClick={() => setShowDesc(true)} onMouseEnter={onMouseEnter}>
        显示描述
      </button>
      {showDesc && <Desc />}
    </div>
  )
}

export default App

按路由代码后能做预加载

上面说的都是页面中某个次要内容的代码分割和预加载,下面来进入正题,按路由切割代码后,能在当前路由预加载其它路由的代码吗?

自己封装一下 Link

我们可以把 react-router-dom 库的 Link 组件再封装一层来实现预加载。

把路径以及对应的组件定义为数组,方便我们封装的 LinkWithPreload 去遍历数组找到组件,然后去执行组件的 preload 就好了。

// App.js
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'
import loadable from '@loadable/component'

export const routes = [
  { path: '/', Component: loadable(() => import('./List')) },
  { path: '/detail/:id', Component: loadable(() => import('./Detail')) },
]

function App() {
  return (
    <Router>
      <Switch>
        {routes.map((item) => {
          const { path, Component } = item
          return (
            <Route key={path} exact path={path}>
              <Component />
            </Route>
          )
        })}
      </Switch>
    </Router>
  )
}

export default App
// LinkWithPreload.js
import { Link, matchPath } from 'react-router-dom'
import { routes } from './App'

function LinkWithPreload(props) {
  const { to } = props

  const onMouseEnter = () => {
    const find = routes.find((item) => {
      const { path } = item
      const match = matchPath(to, {
        path,
        exact: true,
      })
      return Boolean(match)
    })
    if (find) {
      find.Component.preload()
    }
  }

  return <Link {...props} onMouseEnter={onMouseEnter} />
}

export default LinkWithPreload

在需要使用 Link 的地方就 <LinkWithPreload to='/xxx'>查看</LinkWithPreload> 就可以了。

这里是鼠标移上去的时候预加载,如果你想,也可以改为使用 Intersection Observer ,判断 Link 组件进入可见区域时就预加载。

在什么时候进行预加载也是一种权衡,尽早预加载可以保证跳转页面的时候资源已经加载好了,但是会不可避免造成一些不必要的加载,因为你不知道用户会访问哪些页面。(当然如果你想你可以结合统计工具的数据,只对用户经常访问的页面做预加载来增加命中率 hhh)

直接使用 quicklink 库
https://github.com/GoogleChromeLabs/quicklink

它是监听的 Link 进入可视区域就进行预加载。

在 create-react-app 中使用:https://github.com/GoogleChromeLabs/quicklink/blob/master/demos/spa/README.md

使用这个库会需要配置 webpack-route-manifest 插件,这个插件会生成下面这个东西,然后就可以根据路由去预加载了。
image
quicklink 的相关实现见 https://github.com/GoogleChromeLabs/quicklink/blob/master/src/react-chunks.js#L61https://github.com/GoogleChromeLabs/quicklink/blob/master/src/index.mjs#L60

它是等路由组件进入可视区域后,然后拿到路由组件中所有 a 标签,然后再对应去做预加载。

timeoutFn(() => {
  // Find all links & Connect them to IO if allowed
  (options.el || document).querySelectorAll('a').forEach(link => {
    // If the anchor matches a permitted origin
    // ~> A `[]` or `true` means everything is allowed
    if (!allowed.length || allowed.includes(link.hostname)) {
      // If there are any filters, the link must not match any of them
      isIgnored(link, ignores) || observer.observe(link);
    }
  });
}, {
  timeout: options.timeout || 2000,
});

总结

为了减少加载一个页面时需要下载的代码,我们可以:

按路由切割代码,并预加载其它路由代码(Link hover 时或者进入可视区域时),这样跳转时下个页面加载会更快;
对弹窗、Tab 等当前不需要展示或者低优先级内容做代码切割,并预加载。
代码切割是为了减少必要代码的体积,预加载是为了低优先级组件代码在需要时也能尽快展示。

preload、prefetch、动态 import 区别

preload 和 prefetch 是 HTML link 标签的一个用法,用于提示浏览器去提前下载资源。preload 是希望提前下载当前页面的资源。prefetch 是希望提前下载其它页面的资源。

动态 import 是 JS 的一个语法,Webpack 打包时会把动态 import 的部分打包为单独的文件。可以用于实现按路由切割代码,或者把弹窗等低优先级界面代码从主界面代码切割出去,这样来加快主界面的加载速度。

当你希望预加载资源时,是使用 link 的 prefetch 还是说动态 import ,其实结果都是一样的,可以结合项目用的库来看怎么实现简单怎么来。

【算法系列】fib优化

fib性能不好主要是因为会多次计算重复n,因此需要对n的计算结果进行缓存

const fn = (() => {
  const obj = {}
  // obj变量形成闭包,用于缓存fn(n)
  return function(n){
    if(obj[n]) return obj[n] 
    if(n===1) return 0
      else if(n===2) return 1
      else {
        res = fn(n-1) + fn(n-2)
        obj[n] = res
        return res
      }
  }
})()

【Javascript】Typescript 装饰器及应用场景浅析

引言

本文旨在对不同种类的装饰器进行学习, 了解装饰器及装饰器工厂的差别,举例应用场景,并浅析装饰器原理。

一、装饰器种类

  • 1、Class Decorators - 类装饰器

类装饰器在类声明之前声明, 类装饰器应用于类的构造函数,可用于观察、修改或替换类定义。

1.1 类装饰器的表达式将在运行时作为函数调用,被装饰类的构造函数将作为它的唯一参数。

function decorateClass<T>(constructor: T) {
    console.log(constructor === A) // true
}
@decorateClass
class A {
    constructor() {
    }
}

上述代码可以看出类装饰器接收的参数constructor === A.prototype.constructor,即constructorclass A的构造函数。

1.2 如果类装饰器返回一个构造函数, 它会使用提供的构造函数来替换类之前的声明。

function decorateClass<T extends { new (...args: any[]): {} }>(constructor: T){
    return class B extends constructor{
      name = 'B'
    }
}
@decorateClass
class A {
    name = 'A'
    constructor() {
    }
}
console.log(new A().name)  // 输出 B
  • 2、 Method Decorators - 方法装饰器

方法装饰器在方法声明之前声明。装饰器可以应用于方法的属性描述符,并可用于观察、修改或替换方法定义。

2.1 方法装饰器的表达式将在运行时作为函数调用,带有以下三个参数:

  • target: 当其装饰静态成员时为类的构造函数,装饰实例成员时为类的原型对象。
  • key: 被装饰的方法名。
  • descriptor: 成员的属性描述符 即 Object.getOwnPropertyDescriptor(target,key)
function decorateMethod(target: any,key: string,descriptor: PropertyDescriptor){
      console.log('target === A',target === A)  // 是否类的构造函数
      console.log('target === A.prototype',target === A.prototype) // 是否类的原型对象
      console.log('key',key) // 方法名
      console.log('descriptor',descriptor)  // 成员的属性描述符 Object.getOwnPropertyDescriptor
}
class A {
      @decorateMethod  // 输出 true false 'staticMethod'  Object.getOwnPropertyDescriptor(A,'sayStatic')
      static staticMethod(){
      }
      @decorateMethod  // 输出 false true 'instanceMethod'  Object.getOwnPropertyDescriptor(A.prototype,'sayInstance')
      instanceMethod(){
      }
}

2.2 如果方法装饰器返回一个值,它会被用作方法的属性描述符。

function decorateMethod(target: any,key: string,descriptor: PropertyDescriptor){
   return{
     value: function(...args: any[]){
         var result = descriptor.value.apply(this, args) * 2;
         return result;
     }
   }
}
class A {
   sum1(x: number,y: number){
       return x + y
   }
   
   @decorateMethod
   sum2(x: number,y: number){
     return x + y
   }
}
console.log(new A().sum1(1,2))  // 输出3
console.log(new A().sum2(1,2))  // 输出6

上述代码可以看出sumdecorateMethod装饰后,其返回值发生了变化

  • 3、Accessor Decorators - 访问器装饰器

访问器装饰器在访问器声明之前声明。访问器装饰器应用于访问器的属性描述符,并可用于观察、修改或替换访问器的定义。

3.1 访问器装饰器与方法装饰器有诸多类似,接受3个参数:

  • target: 当其装饰静态成员时为类的构造函数,装饰实例成员时为类的原型对象。
  • key: 被装饰的成员名。
  • descriptor: 成员的属性描述符 即 Object.getOwnPropertyDescriptor(target,key)
function configurable (target: any, key: string, descriptor: PropertyDescriptor) {
       descriptor.configurable = false
};
class A {
       _age = 18
       get age(){
         return this._age
       }
       @configurable
       set age(num: number){
         this._age = num
       }
}

3.2 如果访问器装饰器返回一个值,它会被用作访问器的属性描述符。

function configurable (target: any, key: string, descriptor: PropertyDescriptor) {
    return {
      writable: false
    }
};
class A {
    _age = 18
    @configurable
    get age(){
       return this._age
    }
    set age(num: number){
       this._age = num
    }
}
const a = new A()
a.age = 20   // 抛出 TypeError: Cannot assign to read only property 'age'
  • 4、Property Decorators - 属性装饰器

属性装饰器在属性声明之前声明,返回值会被忽略。

4.1 属性装饰器的表达式将在运行时作为函数调用,带有以下两个参数:

  • target: 当其装饰静态成员时为类的构造函数,装饰实例成员时为类的原型对象。
  • key: 被装饰的成员名。
function decorateAttr(target: any, key: string) {
      console.log(target === A)
      console.log(target === A.prototype)
      console.log(key)
}
class A {
      @decorateAttr // 输出 true false staticAttr
      static staticAttr: any
      @decorateAttr // 输出 false true instanceAttr
      instanceAttr: any
}
  • 5、Paramter Decorators - 参数装饰器

参数装饰器在参数声明之前声明,返回值会被忽略。

5.1 参数装饰器的表达式将在运行时作为函数调用,带有以下三个参数:

  • target: 当其装饰静态成员时为类的构造函数,装饰实例成员时为类的原型对象。
  • key: 参数名。
  • index: 参数在函数参数列表中的索引。
function required(target: any, key: string, index: number) {
      console.log(target === A)
      console.log(target === A.prototype)
      console.log(key)
      console.log(index)
}
class A {
      saveData(@required name: string){}  // 输出 false true name 0
}

二、装饰器工厂

不同类型装饰器本身参数是固定的,在运行时被调用,当我们需要自定义装饰器参数时,便可以来构造一个装饰器工厂函数,如下便是一个属性装饰器工厂函数,支持自定义传参nameage

function decorateAttr(name: string, age: number) {
      return function (target: any, key: string) {
        Reflect.defineMetadata(key, {
          name, age
        }, target);
      }
}

三、执行顺序

ts规范规定装饰器工厂函数从上至下开始执行,装饰器函数从下至上开始执行

function first() {
  console.log("first(): factory evaluated");
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("first(): called");
  };
}
 
function second() {
  console.log("second(): factory evaluated");
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("second(): called");
  };
}
 
class ExampleClass {
  @first()
  @second()
  method() {}
}

控制台输出如下,类似中间件的洋葱模型

first(): factory evaluated
second(): factory evaluated
second(): called
first(): called

四、应用场景

  1. 逻辑层消除繁琐的try/catch块,装饰器内统一输出函数日志
function log (target: any, key: string, value: PropertyDescriptor){
      return {
          value: async function (...args) {
            try{
              await value.value.apply(this, args)
            }catch(e){
              console.log(e)
            }
          }
      };
  };

  class A {
    @log
    syncHandle(){
      return 3 + a
    }
    
    @log
    asyncHandle(){
      return Promise.reject('Async Error')
    }
  }

new A().syncHandle()
new A().asyncHandle()

控制台输出如下:

  1. 校验参数或返回值类型
function validate(){
  return function (target: any, name: string, descriptor:PropertyDescriptor) {
    let set = descriptor.set
    descriptor.set = function (value) {
      let type = Reflect.getMetadata("design:type", target, name);
      console.log(type.name)
      if (!(new Object(value) instanceof type)) {
        throw new TypeError(`Invalid type, got ${typeof value} not ${type.name}.`);
      }
      set?.call(this, value);
    }
  }
}
class A {
    _age: number

    constructor(){
      this._age = 18
    }

    get age(){
      return this._age
    }

    @validate()
    set age(value: number){
      this._age = value
    }
}
const a= new A()
a.age = 30 
a.age = '30'  // 抛出 TypeError: Invalid type, got string not Number.

【CoolShell】别自己墙了自己

  • 做有价值的事。

这个世界对计算机人才的要求是供不应求的,所以,不要让自己为自己找各式各样的借口,让自己活在“玩玩具”、“搬砖”和“使蛮力加班”的境地。其实,我发现这世界上有能力的人并不少,但是有品味的人的确很少。所谓的有价值,就是,别人愿付高价的,高技术门槛的,有创造力的,有颠覆性的……

  • 扩大自己的眼界,开放自己的内心。

人要变得开放,千万不要做一个狭隘的民族主义者,做一个开放的人,把目光放在全人类这个维度,不断地把自己融入到世界上,而不是把自己封闭起来,这里,你的英文语言能力对你能不能融入世界是起决定性的作用。开放自己的心态,正视自己的缺点,你才可能往前迈进。你的视野决定了你的知不知道要去哪,你的开放决定了你想不想去。

  • 站在更高的维度。

面的维度会超过点的维点,空间的维度会超过面的维度,在更高维度上思考和学习,你会获得更多。整天在焦虑那些低维度的事(比如自己的薪水、工作的地点、稳不稳定、有没有户口……),只会让你变得越来越平庸,只要你站在更高的维度(比如: 眼界有没有扩大、可能性是不是更多、竞争力是不是更强、能不能解决更大更难的问题、能创造多大的价值……),时间会让你明白那些低维度的东西全都不是事儿。技术学习上也一样,站在学习编程语法特性的维度和站在学习编程范式、设计模式的维度是两种完全不一样的学习方式。

  • 精于计算得失。

很多人其实不是很懂计算。绝大多数人都是在算计自己会失去多少,而不会算会得到多少。而一般的人也总是在算短期内会失去什么,优秀则总是会算我投入后未来会有什么样的回报,前者在算计今天,目光短浅,而后者则是舍在今天,得在明天,计算的是未来。精于计算得失的,就懂得什么是投资,不懂的只会投机。对于赚钱,你可以投机,但是对于自己最好还是投资。

  • 勇于跳出传统的束缚。

有时候,跳出传统并不是一件很容易的事,因为大多数人都会对未知有恐惧的心理。比如:我看到很多人才都被大公司垄断了,其实,有能力的人都不需要加入大公司,有能力的人是少数,这些少数的人应该是所有的公司share着用的,这样一来,对于所有的人都是利益最大化的。这样的事现在也有,比如:律师、设计师……。但是,绝大多数有能力的技术人员是不敢走出这步。我在2015年到2016年实践过一年半,有过这些实践,做“鸡”的比“二奶”好多了,收入也好很多很多(不好意思开车了)……

井蛙不可以语于海者,拘于虚也;//空间局限

夏虫不可以语于冰者,笃于时也;//时间局限

曲士不可以语于道者,束于教也。//认识局限

别自己墙了自己,人最可悲的就是自己限制自己,想都不敢想,共勉!

【python】使用简单的python语句编写爬虫 定时拿取信息并存入txt

爬虫脚本 echo2.py

# -*- coding: utf-8 -*-    #解决编码问题
import urllib
import urllib2
import re
import os
import time
 
page = 1
url = 'http://www.qiushibaike.com/text/page/4/?s=4970196'     #爬取的目标网站
user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
headers = { 'User-Agent' : user_agent }
try:
    request = urllib2.Request(url,headers = headers)
    response = urllib2.urlopen(request)
    # print response.read()
    content = response.read().decode('utf-8')  #解决编码问题
    pattern = re.compile(r'<div.*?class="content".*?<span>(.*?)</span>.*?</div>',re.S)  #第一个参数是匹配要爬取的内容,这里使用正则去匹配
    items = re.findall(pattern,content)   
    f=open(r'.\article.txt','ab')       #txt文件路径
    nowTimes = time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time()))    #获取当前时间
    f.write('时间:{}\n\n'.format(nowTimes),);   #txt文件中写入时间
    for i in items:
        i.encode('utf-8')
        agent_info = u''.join(i).encode('utf-8').strip()
        f.writelines('段子:%s%s\n'%(str(agent_info),os.linesep))   #分行存入
        # f.write('%s'%str(agent_info))
    f.close()   
    
    # print items
   
except urllib2.URLError, e:
    if hasattr(e,"code"):
        print e.code
    if hasattr(e,"reason"):
        print e.reason

布置定时任务使用crontab。 (具体crontab使用方法可见http://blog.csdn.net/daivon_up/article/details/71266814):

* */1 * * * /usr/bin/python /home/dengwen/desktop/echo2.py

运行结果:

运行结果

如果觉得我的文章对你有帮助,欢迎关注我的blog

相关知识点

【Javascript】深入理解async/await的实现,Generator+Promise = Async/Await
【Javascript】深入理解this作用域问题以及new运算符对this作用域的影响
【Javascript】手写运算符new创建实例并实现js继承

【javascript】手把手教你使用React Hooks构建Redux进行状态管理

前言

我们在做一个大型的复杂应用时,往往有很多数据会在多个页面多个组件中同时被使用,这时如果仍然使用props传参的方式,就会显得组件之间耦合度过高,且开发效率低

为了解决这个问题,2014年 Facebook 提出了 Flux 架构的概念,引发了很多的实现。2015年,Redux 出现,将 Flux 与函数式编程结合一起,很短时间内就成为了最热门的前端架构。

目录

  • React Hooks
  • Hooks API
  • Hooks 构建 Redux
  • 完整DEMO

React Hooks

hooks 是react 16.8 引入的特性,他允许你在不写class的情况下操作state 和react的其他特性。
hooks 只是多了一种写组件的方法,使编写一个组件更简单更方便,同时可以自定义hook把公共的逻辑提取出来,让逻辑在多个组件之间共享。

Hooks API

基础Hooks

  • useState - 声明state变量
const { state, setState } = useState(initialState)
  • useEffect - Effect钩子允许你在函数组件中执行副作用
useEffect(didUpdate,[])   //didUpdate为要做的更新,[]为要监听的变量
  • useContext - 接收一个Context对象(React.createContext返回的值)并且返回当前<MyContext.Provider>的值, 返回值取决于最近的 <MyContext.Provider>
const  store = useContext(Context)

附加Hooks

  • useReducer - 接收一个(state, action) => newState类型的reducer,并且返回一个与dispatch方法配对的state对象(如果你熟悉redux,那么你已经会了)
const [state,dispatch] = useReducer(reducers,initialState)
  • useCallback - 类似userEffect,与前者相比不能执行副作用,返回缓存的函数
  • useMemo - 类似userEffect,与前者相比不能执行副作用,返回缓存的变量
  • useRef - 类似vue中的$refs,用于操作dom
  • useImperativeHandle - 因为函数式组件不存在组件实例,所以正常情况下ref无法调用子组件的函数,useImperativeHandle返回了一个回调函数帮助我们解决此类问题
function Com(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
Com = forwardRef(Com);
  • useLayoutEffect - 类似componentDidMount/Update, componentWillUnmount,会在dom元素更新后立即执行回调函数
  • useDebugValue - 用于自定义Hooks

Hooks 构建 Redux

Redux 规定,将模型的更新逻辑全部集中于一个特定的层(Flux 里的 store,Redux 里的 reducer)。Flux 和 Redux 都不允许程序直接修改数据,而是用一个叫作 “action” 的普通对象来对更改进行描述。

需要用到的API有createContext,useContext,useReducer

step1 - 首先,我们创建state

state.js

export default  {
  username:''
}

step2 - Redux 规定不允许程序直接修改数据,而是用一个叫作 action 的普通对象来对state进行更改

action.js

export  default {
  setUserName:function(payload){
    return {
      type:'setUserName',
      payload
    }
  }
}

step3 - 将模型的更新逻辑全部集中于一个特定的层(Flux 里的 store,Redux 里的 reducer

reducer.js

export default function reducer(state,action){
  const { payload } = action
  switch(action.type)
  {
    case 'setUserName':     // 匹配action中的type,return新的state
      return {
        ...state, 
        username: payload
      }
    default:
      return state
  }
}

step4 - 使用useReducer获取state以及用于事件派发更新state的dispatch函数

index.js

const [state,dispatch] = useReducer(reducers,initialState)

step5 - 允许所有子组件访问state及dispatch

这里我们需要排除props传参的方式,因为这样就失去了使用redux进行状态管理的意义,这里就需要用到Context
index.js

import React,{createContext, useReducer} from 'react'
import reducers from './reducers'
import initialState from './state'

export const AppContext = createContext({})
export const Consumer =  AppContext.Consumer
export function Provider(props){
  const [state,dispatch] = useReducer(reducers,initialState)
  const store = { state, dispatch } 
  return (
  <AppContext.Provider value={ store }>
    {props.children}
  </AppContext.Provider>
  )
}

这里将useReducer的返回值放到Provider的value中,子组件通过ConsumeruseContext就能访问到了

整个Redux由state、action、reducer、index构成

完整DEMO

1. index.js - 将组件放置在中

import React from 'react'
import ReactDOM from 'react-dom'
import App from './App';
import { Provider } from './redux'

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

2. app.js - 将组件放置在中,并更新state

import React, { useContext } from 'react';
import { AppContext } from './redux'
import Child from './child'
import actions from './redux/actions'

function App() {
  const { dispatch } = useContext( AppContext )   // 获取事件派发函数
  function handleInput(e){
    dispatch(actions.setUserName(e.target.value))   // 派发更新事件并传值
  }
  return (
    <div className="App">
        <input onInput={handleInput} type='text'  />
        <Child  />
    </div>
  );
}
export default App;

3. child.js - 获取Redux中的username

import React, { useContext } from 'react';
import { AppContext } from './redux'

export default function Children(){
  const { state } = useContext(AppContext)
  return (
    <div>Name:{state.username}</div>
  )
}
  1. DEMO源码

【javascript】一篇教你从防抖、节流函数来认识this指向的文章

目录

防抖、节流函数在我们频繁做出某一行为(如用户input框输入或滚动事件)时会经常用到,本篇文章将带你重新认识:

  • 什么是防抖
  • 什么是节流
  • 防抖、节流函数需要注意的细节
    • 为什么需要使用fn.apply
    • 为什么要使用箭头函数
    • 不使用箭头函数如何实现
    • 哪里不能使用箭头函数

什么是防抖

防抖:行为发生一定时间后触发事件,在这段时间内如果该行为再次发生,则重新等待一定时间后再触发事件

// 防抖
function debounce(fn,time){
  let timer = null
  return function(){
    timer && clearTimeout(timer)
    timer = setTimeout(()=>{
      fn.apply(this,arguments)
    },time)
  }
}

什么是节流

节流:一段时间内如果行为多次发生,只会触发一次事件

function throttle(fn,time){
  let oldTimestamp = 0
  return function(){
    const curTimestamp = Date.now()
    if(curTimestamp - oldTimestamp >= time){
      oldTimestamp = curTimestamp
      fn.apply(this,arguments)
    }
  }
}

防抖、节流函数需要注意的细节

这里有一段demo代码

const op = document.getElementById('demo')
op.addEventListener('click',throttle(sayHi,1000))

function sayHi(){
  console.log('sayHi',this)  
}

function debounce(fn,time){
  let timer = null
  return function(){
    timer && clearTimeout(timer)
    timer = setTimeout(()=>{
      fn.apply(this,arguments)
    },time)
  }
}

接着我们继续玩下看

  • 为什么需要使用fn.apply
    fn.apply主要用于帮我们修改函数运行的this指向, 否则this将指向window

  • setTimeout为什么要使用箭头函数
    不使用箭头函数的话,setTimeout内部this将指向window

  • setTimeout不使用箭头函数如何实现debounce

const self = this
fn.apply(self,arguments)
  • 哪里不能使用箭头函数
    return function(){} !== return ()=>{} ,这里如果return一个箭头函数,内部this指向也将指向window

【Javascript】Custom Hook 与 HOC 让代码不再臃肿、冗余

引言

为了更高效地开发、维护前端代码,我们最常听到也最常用的方式就是将通用性较强的组件或逻辑提取成公共函数或公共组件,减少重复代码,那么还有别的方式吗?本文将带你通过Custom HookHOC实现极致的逻辑复用,大幅提升代码开发及维护体验。
在了解具体细节前,我们需要先了解Custom HookHOC

Custom Hook

通过Custom Hook,当两个函数之间存在共享逻辑时,我们可以把共享逻辑提取到第三个函数中,而组件和 Hook 都是函数,所以也同样适用这种方式,即支持 Hook 内 调用其他 Hook

Custom Hook 必须以 “use” 开头,这个约定非常重要。不遵循的话,由于无法判断某个函数是否包含对其内部 Hook 的调用,React 将无法自动检查你的 Hook 是否违反了 Hook 的规则

示例代码如下, 下述组件A、B存在逻辑复用问题 :

组件A

function A() {
  const [isShow, switchShow] = useState(false);
  const switch = () => {
     switchShow(!isShow)
 }
  return <div onClick={switch}>组件A</div> ;
}

组件B

function B() {
  const [isShow, switchShow] = useState(false);
  const switch = () => {
     switchShow(!isShow)
  }
  return <div onClick={switch}>组件B</div> ;
}

解决方案:

自定义 hook useShowStatus

function useShowStatus(defaultStatus){
  const [isShow, switchShow] = useState(defaultStatus);
  const switch = () => {
     switchShow(!isShow)
  }
  return { isShow, switch }
}

组件A

function A() {
  const { isShow, switch }= useShowStatus(false);
  return <div onClick={switch}>组件A</div> ;
}

组件B

function B() {
  const { isShow, switch }= useShowStatus(false);
  return <div onClick={switch}>组件B</div> ;
}

js逻辑重复的问题解决了!,那么如果是以下情况出现了组件引用的多次复用,又该怎么解决呢?

组件A

import { Modal, Button } from 'antd'
function A(){
  const { isShow, switch }= useShowStatus(false);
  return <div>
      <Button onClick={switch}>组件A</Button>
      <Modal show={isShow} />
    </div> ;
}

组件B

import { Modal, Button } from 'antd'
function B(){
  const { isShow, switch }= useShowStatus(false);
  return <div onClick={switch}>
      <Button onClick={switch}>组件B</Button>
      <Modal show={isShow} />
    </div> ;
}

这时便可以用到HOC了。

HOC

HOC又称高阶组件,它接收一个函数作为参数,并且返回值也是一个函数,

高阶组件 HocCom

import { Modal } from 'antd'
function Hoc(Com){
  return function HocCom(props){
    const { isShow, switch }= useShowStatus(false);
    const hocComProps = {
      ...props,
      switch
    }
    return <>
      <Modal show={isShow}  />
      <Com {...hocComProps} />
    </>
  }
}
export default Hoc

组件A

import Hoc from './HocCom'
function A(props){
  const { switch }= props
  return <div>
      <Button onClick={switch}>组件A</Button>
    </div> ;
}
Hoc(A)

组件B

import Hoc from './HocCom'
function B(props){
  const { switch }= props
  return <div>
      <Button onClick={switch}>组件B</Button>
    </div> ;
}
Hoc(B)

总结

如此一来,公共组件及相关状态都放到了HOC中进行管理,大幅提升了代码简洁程度,同时也提升了代码维护效率及开发者开发体验。以上便是Custom Hook 与 HOC 让代码不再臃肿、冗余的代码实践了,如果读者有不同的想法或意见,欢迎在评论区提出,互相学习。

【python】使用简单的python语句编写爬虫 定时拿取信息并存入txt

爬虫脚本 echo2.py

# -*- coding: utf-8 -*-    #解决编码问题
import urllib
import urllib2
import re
import os
import time
 
page = 1
url = 'http://www.qiushibaike.com/text/page/4/?s=4970196'     #爬取的目标网站
user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
headers = { 'User-Agent' : user_agent }
try:
    request = urllib2.Request(url,headers = headers)
    response = urllib2.urlopen(request)
    # print response.read()
    content = response.read().decode('utf-8')  #解决编码问题
    pattern = re.compile(r'<div.*?class="content".*?<span>(.*?)</span>.*?</div>',re.S)  #第一个参数是匹配要爬取的内容,这里使用正则去匹配
    items = re.findall(pattern,content)   
    f=open(r'.\article.txt','ab')       #txt文件路径
    nowTimes = time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time()))    #获取当前时间
    f.write('时间:{}\n\n'.format(nowTimes),);   #txt文件中写入时间
    for i in items:
        i.encode('utf-8')
        agent_info = u''.join(i).encode('utf-8').strip()
        f.writelines('段子:%s%s\n'%(str(agent_info),os.linesep))   #分行存入
        # f.write('%s'%str(agent_info))
    f.close()   
    
    # print items
   
except urllib2.URLError, e:
    if hasattr(e,"code"):
        print e.code
    if hasattr(e,"reason"):
        print e.reason

布置定时任务使用crontab。 (具体crontab使用方法可见http://blog.csdn.net/daivon_up/article/details/71266814):

* */1 * * * /usr/bin/python /home/dengwen/desktop/echo2.py

运行结果:

运行结果

本篇文章如对您有用,欢迎关注我的blog~,谢谢!

【算法系列 - LeetCode】Fizz Buzz 问题

题目

给你一个整数n. 从 1 到 n 按照下面的规则打印每个数:

  • 如果这个数被3整除,打印fizz
  • 如果这个数被5整除,打印buzz.
  • 如果这个数能同时被3和5整除,打印fizz buzz.
  • 如果这个数既不能被 3 整除也不能被 5 整除,打印数字本身。

比如 n = 15, 返回一个字符串数组:

[
  "1", "2", "fizz",
  "4", "buzz", "fizz",
  "7", "8", "fizz",
  "buzz", "11", "fizz",
  "13", "14", "fizz buzz"
]

JS实现1

const fn = function (n) {
    let arr = []
   
    if(n%3 === 0 && n%5 === 0&&n!=0)
    {
    	arr.push('fizz buzz')
    }
    else if(n%3 === 0&&n!=0)
    {
        arr.push('fizz') 
    }
    else if(n%5 === 0&&n!=0)
    {
    	arr.push('buzz')
    }
    else if(n!=0){
    	arr.push(n.toString())
    }

    if(n>1)
    {
    	return  [...arr,...fn(n-1)]
    }
    return arr

}

JS实现2

const fizzBuzz = function(n){
	const arr = []
	for(var i =1;i<=n;i++)
	{
	    if(i%3 === 0 && i%5 === 0)
	    {
	    	arr.push('fizz buzz')
	    }
	    else if(i%3 === 0)
	    {
	        arr.push('fizz') 
	    }
	    else if(i%5 === 0)
	    {
	    	arr.push('buzz')
	    }
	    else{
	    	arr.push(i.toString())
	    }
	}
	return arr
}

【Javascript】探究bind()的作用及实现原理

bind()的作用

在js中,我们通常使用bind()来修改this指向

bind()方法创建一个新的函数,在bind()被调用时,这个新函数的this被bind的第一个参数指定,其余的参数将作为新函数的参数供调用时使用。
我们来看1个demo

var module = {
  x: 42,
  getX: function() {
    return this.x;
  }
}

module.getX.prototype.sayHi = function(name){
  return 'I am ' + name
}

var unboundGetX = module.getX
console.log(module.getX(),new module.getX().sayHi('module.getX'))   // 42 "I am module.getX"   
console.log(unboundGetX(),new unboundGetX().sayHi('unboundGetX'))    // undefined "I am unboundGetX"

unboundGetX()的this指向window,所以输出this.x为undefined,现在,我们使用bind来修改unboundGetX()的this指向

var boundGetx = module.getX.bind(module)  //this指向变量module
console.log(boundGetx(),new boundGetx().sayHi('boundGetx'))   //42 "I am boundGetx"

可以看到bind()返回的函数,其this指向已经指向了module,this.x为=42

不了解this作用域的同学可以先看一下这篇文章【Javascript】深入理解this作用域问题并探究new/let/var/const对this作用域的影响

bind()的实现原理

现在,我们来尝试实现一个newbind(),使得boundGetx的this指向变量module

var boundGetx = module.getX.newbind(module)   

newbind()实现

Function.prototype.newbind = function(){
  const oThis = Array.prototype.shift.call(arguments)  // 获取参数module
  const params = Array.prototype.slice.call(arguments)    
  oThis.fn = this   //函数function
  const res = function(){
    return oThis.fn.apply(
      oThis,params.concat(Array.prototype.slice.call(arguments))
    )   //修改函数function的this指向并执行

    // return oThis.fn(...params.concat(Array.prototype.slice.call(arguments)))   //一样

  }
  if(this.prototype)
  {
     const fn = function(){}
     fn.prototype = this.prototype  //维护原型关系
     res.prototype === new fn()
  }
  return res
}

此时newbind已经返回了1个新的函数

验证newbind正确性

var unboundGetX = module.getX
var boundGetx = module.getX.newbind(module)   

console.log(module.getX(),new module.getX().sayHi('module.getX'))   // 42 "I am module.getX"   
console.log(unboundGetX(),new unboundGetX().sayHi('unboundGetX'))    // undefined "I am unboundGetX"
console.log(boundGetx(),new boundGetx().sayHi('boundGetx'))   //42 "I am boundGetx"

可以发现unboundGetX()仍然输出undefined,但是boundGetx()其this已经指向了module,this.x为42

注意事项

  • res.prototype = this.prototype 存在问题,由于对象属于引用类型,这样的写法会导致module.getX.prototype会随着boundGetx.prototype改变,我们尝试修改一下boundGetx.prototype
boundGetx.prototype.sayHi = function(name){
  return 'changed,I am ' + name
}

console.log(new boundGetx().sayHi('boundGetx'))  // changed,I am boundGetx
console.log(new module.getX().sayHi('module.getX')) //changed,I am module.getX

可以发现boundGetx()module.getX()sayHi()都被改变了,我们需要将代码改为

    res.prototype = new this()  //维护原型关系

    // or
    // const fn = function(){}   
    // fn.prototype = this.prototype
    // res.prototype = new fn()

bind与call,apply的差别

bind返回的是1个函数,call,apply返回值为undefined

Function.prototype.call = function(obj,...args){
	obj.fn = this
	obj.fn(...args)
	delete obj.fn
}

完整代码

codesandbox 运行代码

Function.prototype.newbind = function(){
  const oThis = Array.prototype.shift.call(arguments)  // 第1个参数
  const params = Array.prototype.slice.call(arguments)
  oThis.fn = this   //函数function
  const res = function(){
    return oThis.fn.apply(
      oThis,params.concat(Array.prototype.slice.call(arguments))
    )   //修改函数function的this指向并执行
  }
  if(this.prototype)
  {
    res.prototype = new this()  //维护原型关系
  }
  return res
}


var module = {
  x: 42,
  getX: function(val) {
    return val ? val : this.x 
  }
}
module.getX.prototype.sayHi = function(name){
  return 'I am ' + name
}

var unboundGetX = module.getX;
//使用bind修改unboundGetX的this指向
var boundGetx = module.getX.newbind(module)

console.log(module.getX(),new module.getX().sayHi('module.getX'))     //this指向变量module
console.log(unboundGetX(),new unboundGetX().sayHi('unboundGetX'))     //this指向window,所以为undefined
console.log(boundGetx(),new boundGetx().sayHi('boundGetx'))   //this指向变量modul
console.error('---------sayHi was changed------')
boundGetx.prototype.sayHi = function(name){
  return 'changed,I am ' + name
}
console.log(new boundGetx().sayHi('boundGetx'))
console.log(new module.getX().sayHi('module.getX'))

【CSS】从浏览器渲染层面解析css3动效优化原理

引言

在h5开发中,我们经常会需要实现一些动效来让页面视觉效果更好,谈及动效便不可避免地会想到动效性能优化这个话题:

  • 减少页面DOM操作,可以使用CSS实现的动效不多出一行js代码
  • 使用绝对定位脱离让DOM脱离文档流,减少页面的重排(relayout)
  • 使用CSS3 3D属性开启硬件加速

那么,CSS3与动效优化有什么关系呢,本文将从浏览器渲染层面讲述CSS3的动效优化原理

浏览器页面展示过程

首页,我们需要了解一下浏览器的页面展示过程:
浏览器页面展示过程

  • Javascript:主要负责业务交互逻辑。
  • Style: 根据 CSS 选择器,对每个 DOM 元素匹配对应的 CSS 样式。
  • Layout: 具体计算 DOM 元素显示在屏幕上的大小及位置。
  • Paint: 实现一个 DOM 元素的可视效果(颜色、边框、阴影等),一般来说由多个渲染层完成。
  • Composite: 当每个层绘制完成后,浏览器会将所有层按照合理顺序合并为一个图层,显示到屏幕。
    本文我们将重点关注 Composite 过程。

浏览器渲染原理

在讨论 Composite 之前,我们还需要了解一下浏览器渲染原理
浏览器渲染原理

从该图中,我们可以发现:

  • DOM 元素Layout Object 存在一一对应的关系
  • 一般来说,拥有相同坐标空间的 Layout Object 属于同一个 Paint Layer (渲染层),通过 position、opacity、filter等 CSS 属性可以创建新的 Paint Layer
  • 某些特殊的 Paint Layer 会被认为是 Composite Layer (合成层/复合层),Composite Layer 拥有单独的 Graphics Layer (图形层),而那些非 Composite Layer 的 Paint Layer,会与拥有 Graphics Layer 的父层共用一个

Graphics Layer

我们日常生活中所看到屏幕可视效果可以理解为:由多个位图通过 GPU 合成渲染到屏幕上,而位图的最小单位是像素。如下图:

那么位图是怎么获得的呢,Graphics Layer 便起到了关键作用,每个 Graphics Layer 都有一个 Graphics Context, 位图是存储在共享内存中,Graphics Context 会负责将位图作为纹理上传到GPU中,再由GPU进行合成渲染。如下图:

CSS在浏览器渲染层面承担了怎样的角色

大多数人对于CSS3的第一印象,就是可以通过3D(如transform)属性来开启硬件加速,许多同学在重构某一个项目时,考虑到动画性能问题,都会倾向将2D属性改为3D属性,但开启硬件加速的底层原理其实就在于将 Paint Layer 提升到了 Composite Layer

以下的几种方式都用相同的作用:

  • 3D属性开启硬件加速(3d-transform)
  • will-change: (opacity、transform、top、left、bottom、right)
  • 使用fixed或sticky定位
  • 对opacity、transform、filter应用了 animation(actived) or transition(actived),注意这里的animation及transition需要是激活状态才行

我们来写两段demo代码,带大家具体分析一下实际情况

demo1. 3D属性开启硬件加速(3d-transform)

.composited{
  width: 200px;
  height: 200px;
  background: red;
  transform: translateZ(0)
}
</style>

<div class="composited">
  composited - 3dtransform
</div>


可以看到是因为使用的CSS 3D transform,创建了一个复合层

demo2. 对opacity、transform、filter应用 animation(actived) or transition(actived)

<style>
@keyframes move{
  0%{
    top: 0;
  }
  50%{
    top: 600px;
  }
  100%{
    top: 0;
  }
}
@keyframes opacity{
  0%{
    opacity: 0;
  }
  50%{
    opacity: 1;
  }
  100%{
    opacity: 0;
  }
}

#composited{
  width: 200px;
  height: 200px;
  background: red;
  position: absolute;
  left: 0;
  top: 0;
  
}
.both{
  animation: move 2s infinite, opacity 2s infinite;
}
.move{
  animation: move 2s infinite;
}
</style>

<div  id="composited" class="both">
  composited - animation
</div>
<script>
setTimeout(function(){
  const dom = document.getElementById('composited')
  dom.className = 'move'
},5000)
</script>

这里我们定义了两个keyframes(move、opacity),还有两个class(both、move),起初 #compositedclassName = 'both',5秒延时器后,className = 'move',我们来看看浏览器的实际变化。

起初:#composited 创建了一个复合层,并且运动时 fps 非常稳定
起初

5秒后:复合层消失,运动时 fps 会发生抖动
5秒后

如何查看复合层及fps

动画性能最优化

之前,我们提到了页面呈现出来所经历的渲染流水线,其实从性能方面考虑,最理想的渲染流水线是没有布局和绘制环节的,为了实现上述效果,就需要只使用那些仅触发 Composite 的属性。

目前,只有两个属性是满足这个条件的:transformsopacity
相关信息可查看:css Triggers

总结

提升为合成层简单说来有以下几点好处:

  • 合成层的位图,会交由 GPU 合成,比 CPU 处理要快
  • 当需要 repaint 时,只需要 repaint 本身,不会影响到其他的层
  • 对于 transform 和 opacity 效果,部分浏览器不会触发 Layout 和 Paint, 相关信息可查看:css Triggers

缺点:

  • 创建一个新的合成层并不是免费的,它得消耗额外的内存和管理资源。
  • 纹理上传后会交由 GPU 处理,因此我们还需要考虑 CPU 和 GPU 之间的带宽问题、以及有多大内存供 GPU 处理这些纹理的问题

大多数人都很喜欢使用3D属性 translateZ(0) 来进行所谓的硬件加速,以提升性能。但我们还需要切实的去分析页面的实际性能表现,不断的改进测试,这样才是正确的性能优化途径。

参考资料

无线性能优化:Composite

【Javascript】探究javascript中的堆/栈/任务队列与并发模型 event loop的关系

堆/栈/队列

在javascript中,存在调用栈 (call stack)内存堆(memory heap) ,程序中函数依次进入栈中等待执行,若执行时遇到异步方法,该异步方法会被添加到用于回调的任务队列(task queue)中,【即JavaScript执行引擎的单线程拥有一个调用栈、内存堆和一个任务队列】

调用栈 (call stack):CallStack是用来处理函数调用与返回的。特点是先进后出,每次调用一个函数,Javascript运行时会生成一个新的调用结构压入CallStack。而函数调用结束返回时,JavaScript运行时会将栈顶的调用结构弹出。由于栈的LIFO特性,每次弹出的必然是最新调用的那个函数的结构。函数调用会形成了一个堆栈帧,存放基本数据类型的变量

内存堆(memory head):引用数据类型被存放在堆中,在我们进行浅复制时,我们改变的只是引用数据类型在栈内存中的引用地址,实际上它在堆内存中的引用地址仍然没有发生变化

任务队列(task queue):javaScript 运行时包含了一个待处理的任务队列。

并发模型 与 EventLoop

javascript引擎是单线程的,它的并发模型基于Event Loop(事件循环)

当线程中的同步任务执行完,执行栈为空时,则从任务队列(task queue)中取出异步任务进行处理。这个处理过程包含了调用与这个任务相关联的函数(以及因而创建了一个初始堆栈帧)。当执行栈再次为空的时候,也就意味着该任务处理结束,从任务队列中取出下一个异步任务进行处理,不断重复,这个过程是循环不断的, 所以整个的这种运行机制又称为Event Loop(事件循环).

Task Queue 任务队列

任务队列有宏任务队列微任务队列,

  • 宏任务:script(全局任务), setTimeout, setInterval, setImmediate, I/O, UI rendering.
  • 微任务:process.nextTick, Promise, Object.observer, MutationObserver.
每次事件循环的时候:
  • 微任务/宏任务在相同作用域下,会先执行微任务,再执行宏任务
      setTimeout(()=>{
        console.log('timer')
      })
      Promise.resolve('promise').then((res)=>{
        console.log(res)
      })

  • 宏任务处于微任务作用域下,会先执行微任务,再执行微任务中的宏任务
      Promise.resolve('promise').then((res)=>{
        console.log(res)
        setTimeout(()=>{
          console.log('timer')
        })
      })

  • 微任务处于宏任务作用域下时,会先执行宏任务队列中的任务,然后再执行微任务队列中的任 务,在当前的微任务队列没有执行完成时,是不会执行下一个宏任务的。
        Promise.resolve('promise1').then((res)=>{
          console.log(res)
          Promise.resolve('promise2').then((res)=>{
            console.log(res)
            Promise.resolve('promise3').then((res)=>{
              console.log(res)
            })
          })
          setTimeout(()=>{
            console.log('timer')
          })
        })

【算法系列 - 剑指Offer】从尾到头打印链表

题目

输入一个链表,返回一个反序的链表。

思路

使用generateLinkList生成链表linklist,对链表linklist进行reverse

JS实现

class ListNode{
	constructor(nextIndex,val){
		this.nextIndex = nextIndex
		this.val = val
	}
}


function generateLinkList(length){    //generate linklist
	var list = []
	for(var i=0;i<length;i++)
	{
		// console.log(i)
		var listnode = new ListNode(i+1,i)
		list.push(listnode)
		// console.log(listnode)
	}
	return list
}


const linklist = generateLinkList(10)


function main(linkList){    //reverse linklist
	const result = []
	for(var index=0;index<linkList.length;index++)
	{
		// linkList[index].val 
		var listnode = new ListNode(linkList[index].nextIndex,linkList[linkList.length-index-1].val)
		result.push(listnode)
	}
	return result
}


console.log(main(linklist))
/*function ListNode(x){
    this.val = x;
    this.next = null;
}*/
function printListFromTailToHead(head)
{
    if(!head)
    {
        return 0
    }
    var res = []
    while(head)
    {
        res.push(head.val)
        head = head.next
    }
    return res.reverse()
}

【Javascript】彻底捋清楚javascript中 new 运算符的实现

new 运算符

在js中,new()常被用来创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例

new 关键字会进行如下的操作:

1.创建一个简单JavaScript空对象(即{});
2.链接该对象到另一个对象 (即设置该对象的_proto_为构造函数的prototype);
3.执行构造函数,将构造函数内的this作用域指向1步骤中创建的空对象{};
4.如果构造函数有返回值,则return 返回值,否则return 空对象obj

重点解析:实现一个new

1.我们先来写一个构造函数

function Constructor(obj){
  if(obj){
    return obj
  }
}

2.使用new得到的实例化对象并查看它的__proto__

console.log(new Constructor(123))
console.log(new Constructor({a:123}))
console.log((new Constructor()).__proto__)


可以看到,参数为非对象时,会返回{},参数为对象,返回object

3.现在我们来尝试实现一个new()

function subNew(){
  var obj = {}
  obj.__proto__ = Constructor.prototype
  var res = Constructor.call(obj,...arguments)
  return typeof(res) === 'object'&&res || obj  
//当构造函数有return时,返回return值,没有则返回obj{}
}

4.使用 subNew() 创建实例并对比new()

var obj1 = new Constructor()
var obj2 = subNew()
console.log(new Constructor(123),subNew(123))
console.log(new Constructor({a:123}),subNew({a:123}))
console.log(obj1.__proto__,obj2.__proto__,Constructor.prototype)


可以发现 我们已经成功创建了new的实例,并且obj1,obj2的__proto__都指向Constructor的prototype

5.使用subNew尝试实现JS继承

总结

new 创建了一个对象obj,并将obj的隐式原型__proto__指向构造函数的原型prototype,同时使用call调用父级的构造函数并传参,之后判断call返回值res,若为object类型,则返回res,否则返回obj

如果觉得我的文章对你有帮助,请star我的Blog

【webpack】 从0到1构建,webpack4持久化缓存优化方案,小白也能看懂的构建过程

之前一直对webpack只是了解到一些皮毛,最近抽空学习了一些持久化缓存的必要性以及方案,并自己进行了一次从0到1的项目配置,花了几天的的时间梳理了一下,在此写下分享,方便大家一起交流学习,有写错或理解错的地方还请大佬多多指点。

开题

1. 缓存的作用

每次代码需要更新时,服务器必须重新部署,客户端也必须重新下载资源。因为从网络中获取资源会很慢,这显然非常低效。这就是为什么浏览器会缓存静态资源的原因。但是这样做有一个弊端:如果在部署新的版本中不修改文件名,浏览器会认为它没有更新,就会使用缓存中的版本。

2.webpack配置持久化缓存解决问题

webpack通过hash配置可以帮我们构建出文件名不同的静态资源文件,这样浏览器就能获取到最新的静态资源了。但是这样每次浏览器对静态资源的请求是全量的,如果可以只修改发生变化的文件的hash值,这样就能进行增量请求,可以大大提高我们的web性能,这就我们所说的持久化缓存。

开始构建过程

##(搭建webpack过程)

1.安装webpack
npm init
//全局安装
npm install -g webpack
//安装到你的项目目录
npm install --save-dev webpack
2.新建文件配置文件及打包项目

1.在空目录下新建两个文件

将webpack.config.js拆分为webpack.development.config.js、webpack.production.config.js。【在webpack4中,添加了mode('development','production','none')配置项,同时会自动同步process.env.NODE_ENV的值。】(同命令行 --mode development)

webpack.development.config.js

module.exports = {
    mode: 'deveopment',
	entry: 	__dirname + "/src/index.js",  //入口
	output: {     //出口
		path:__dirname + "/public",
		filename: "bundle.js"
	},
	devtool:'eval-source-map'   //使得编译后的代码可读性更高,也更容易调试
}

webpack.production.config.js


module.exports = {
    mode:'production',
	entry: 	__dirname + "/src/index.js",  //入口
	output: {     //出口
		path:__dirname + "/public",
		filename: "bundle.js"
	},
	devtool:'source-map'   //使得编译后的代码可读性更高,也更容易调试
}

package.json 修改为:

{
  "name": "document",
  "version": "1.0.0",
  "description": "",
  "main": "webpack.development.config.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack --mode development --config webpack.development.config.js",
    "build": "webpack --mode production --config webpack.production.config.js"
  },
  "author": "",
  "license": "ISC"
}

2.在空目录下新建public,src目录
index.html 放在public目录下

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Webpack Sample Project</title>
  </head>
  <body>
    <div id='root'>
    </div>
    <script src="bundle.js"></script>
  </body>
</html>

index.js 放在src目录下

const greeter = require('./Greeter.js');
document.querySelector("#root").appendChild(greeter());

greeter.js 放在src目录下

module.exports = function() {
  var greet = document.createElement('div');
  greet.textContent = "Hi there and greetings!";
  return greet;
};
3.command -1及plugin配置
npm run start

如出现
这里写图片描述
选择webpack-cli

打包成功后,public目录打包出bundle.js,浏览器打开index.html

这里写图片描述

npm run build

.map为源码映射文件,此时查看bundle.js可以看出js代码已被压缩,因为webpack4在production环境下默认配置了压缩plugin:uglifyjs-webpack-plugin,其它默认插件有:

development环境

const webpack = require('webpack')
module.exports = {
    mode: 'development',
	entry: 	__dirname + "/src/index.js",  //入口
	output: {     //出口
		path:__dirname + "/public",
		filename: "bundle.js"
	},
    devtool:'eval-source-map',   //使得编译后的代码可读性更高,也更容易调试
    plugins:[
        // new webpack.NamedModulesPlugin(),    //已默认配置
        // new webpack.NamedChunksPlugin(),   //已默认配置  
        // new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("development") }),  //已默认配置
    ]
}

production环境

const webpack = require('webpack')	
module.exports = {
    mode:'production',
	entry: 	__dirname + "/src/index.js",  //入口
	output: {     //出口
		path:__dirname + "/public",
		filename: "bundle.js"
	},
    devtool:'source-map',   //使得编译后的代码可读性更高,也更容易调试
    plugins:[
        // new UglifyJsPlugin(/* ... */),     //已默认配置   js压缩
        // new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production") }),  //已默认配置   定义变量
        // new webpack.optimize.ModuleConcatenationPlugin(),  //已默认配置  作用域(scope hoisting)提升 
        // new webpack.NoEmitOnErrorsPlugin()   //已默认配置   打包过程中报错不会退出 
    ]
}

mode 为 none时,默认无plugin配置

4.构建本地服务器
npm install  webpack-dev-server --save

webpack.development.config.js 添加

...
	devServer:{
		contentBase:'./public',   //设置本地服务器目录
		historyApiFallback:true,    //设置为true,所有的跳转将指向index.html
		inline:true, //实时刷新
		port:3000, //端口
	},
...

package.json 添加

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack --mode development --config webpack.development.config.js",
    "build": "webpack --mode production --config webpack.production.config.js",
    "dev": "webpack-dev-server --mode development --config webpack.development.config.js --watch "
},
npm run dev

浏览器打开localhost:3000
这里写图片描述
此时我们的本地服务器就构建成功了

5.现在我们尝试配置react开发环境,添加loader、babel对文件内容进行处理 (development、production公用配置)

1.添加config.json文件

{
    "greetText": "Hello World!"
  }

2.修改greeter.js


import React, {Component} from 'react'
import config from './config.json';

class Greeter extends Component{
  render() {
    return (
      <div>
        {config.greetText}
      </div>
    );
  }
}

export default Greeter

3.修改index.js

import React from 'react';
import {render} from 'react-dom';
import Greeter from './greeter';

console.log(document.getElementById('root'))
render(<Greeter />, document.getElementById('root'));

4.安装webpack所需loader、babel

npm install --save-dev babel-core babel-loader babel-preset-env babel-preset-react

5.安装react所需库

npm install --save react react-dom

6.配置webpack

...
module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader",
                    options: {
                        presets: [
                            "env", "react"    //babel
                        ]
                    }
                },
                exclude: /node_modules/
            }
        ]
    }
...
npm run dev

这里写图片描述

此时react运行环境已配置好

6.配置css运行环境(development、production公用配置)

1.src新建greeter.css文件

.root {
    background-color: #eee;
    padding: 100px;
    border: 3px solid #ccc;
  }

2.修改greeter.js

...
	import styles from './greeter.css';
...

3.安装css loader

npm install css-loader style-loader

4.配置loader

...
	module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader",
                    options: {
                        presets: [
                            "env", "react"    //babel
                        ]
                    }
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                    }
                ]
            }
        ]
    },
...
npm run dev

可以看到css已经可以打包运行了

这里写图片描述

持久化缓存优化 (一般production才配置)

1.提取css并压缩

webpack4中已经remove了 extract-text-webpack-plugin,官方推荐使用mini-css-extract-plugin

npm install --save mini-css-extract-plugin optimize-css-assets-webpack-plugin uglifyjs-webpack-plugin
2.配置
const MiniCssExtractPlugin = require("mini-css-extract-plugin");   
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");  
const UglifyJsPlugin = require("uglifyjs-webpack-plugin"); 

...
	optimization: {
        minimizer: [
            new UglifyJsPlugin({     
                cache: true,
                parallel: true,
                sourceMap: false, // set to true if you want JS source maps
                uglifyOptions: {
                    compress: true,
                }
            }),
            new OptimizeCSSAssetsPlugin({})
        ],
    },
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader",
                    options: {
                        presets: [
                            "env", "react"    //babel
                        ]
                    }
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use:[MiniCssExtractPlugin.loader,'css-loader']
            }
        ]
    },
    plugins[
	    new MiniCssExtractPlugin({
            filename: "[name]-[contenthash].css",    
            chunkFilename: "[id].css"
	    }),
    ]
...
npm run build

可以看到css已经被单独提取出来,并且js、css都已经被压缩

这里写图片描述

不过此时我们可以看到react仍然被打包到bundle中去了,导致bundle.js仍然很大,像这种npm包一般不会有修改,所以我们也应该提取出来
在webpack4中,commonsChunkPlugin已被removed,官方推荐使用SplitChunksPlugin

3.提取公共模块
...
	output: {     //出口
		path:__dirname + "/public",
		filename: "[name]-[contenthash].bundle.js",
        chunkFilename: '[name].[chunkhash:5].chunk.js'
	},
...
	optimization: {
        minimizer: [
            new UglifyJsPlugin({     
                cache: true,
                parallel: true,
                sourceMap: false, // set to true if you want JS source maps
                uglifyOptions: {
                    compress: true,
                }
            }),
            new OptimizeCSSAssetsPlugin({})
        ],
        splitChunks: {
            chunks: "all",   //initial、async和all
            minSize: 30000,   //形成一个新代码块最小的体积
            minChunks: 1,  //在分割之前,这个代码块最小应该被引用的次数
            maxAsyncRequests: 5,   //按需加载时候最大的并行请求数
            maxInitialRequests: 3,   //最大初始化请求数
            automaticNameDelimiter: '~',   //打包分割符
            name:'commons',
              //打包后的名字
            cacheGroups: {
                vendors: {
                    test: /[\\/]node_modules[\\/]/,   //用于控制哪些模块被这个缓存组匹配到、它默认会选择所有的模块
                    priority: -10,   //缓存组打包的先后优先级
                    name: 'vendors', 
                    minSize:3000
                },
                default: {
                    minChunks: 2,
                    priority: -20,
                    minSize:3000
                }
            }
        }
    },
...
4.配置化生成index.html (自动更新导入的css、js文件)
npm install html-webpack-plugin
const HtmlWebpackPlugin = require('html-webpack-plugin')
...
	plugins:[
        ...
        new HtmlWebpackPlugin({
            template: __dirname + "/src/index.tmpl.html",//new 
        }),
        ...
    ]
...

src目录新建index.tmpl.html
index.tmpl.html

<!DOCTYPE html>
	<html lang="en">
	<head>
	    <meta charset="UTF-8">
	    <meta name="viewport" content="width=device-width, initial-scale=1.0">
	    <meta http-equiv="X-UA-Compatible" content="ie=edge">
	    <title>Document</title>
	</head>
	<body>
	    <div id="root"></div>
	</body>
</html>
5.查看成果
npm run build

##总结

此时我们可以发现react已被打包到vendors中,bundle.js小了很多,并且多次打包,文件hash值不再修改

这里写图片描述

修改.css文件

这里写图片描述

可以发现只有css的hash值发生改变,其它文件名并未发生变化,当发布静态文件时,浏览器就对未变化的文件仍然使用缓存,这样就符合我们要实现持久化缓存的预期。

【算法系列 - 剑指Offer】斐波那契数列 及其 优化方案

题目描述

大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。
n<=39

JS实现

  • 非递归
function Fibonacci(n){
    if(n == 0){
        return 0
    }else if(n==1)
    {
        return 1
    }else{
    	var arr = []
    	arr[0] = 0
    	arr[1] = 1
    	for(var i = 2;i<=n;i++)
    	{
    		arr[i]= arr[i-1] + arr[i-2]
    	}
    	return arr[n]
    }
}
  • 递归

优化前

function Fibonacci(n){
    if(n == 0){
        return 0
    }else if(n==1)
    {
        return 1
    }else{
        return generate(n-1)+generate(n-2)
    }
}

优化方案1:
尾递归优化,节省内存空间

function Fibonacci(n)
{
    // write code here
   function get(index,last1=0,last2=1){
    	if(index <= 0){
	        return last1
	    }else if(n==1)
	    {
	        return last2
	    }else{
	        return get(index-1,last2,last2+last1)
	    }
    }
    return get(n)
}

优化方案2:
fib性能不好主要是因为会多次计算重复n,因此需要对n的计算结果进行缓存

const Fibonacci = (() => {
  const obj = {}
  // obj变量形成闭包,用于缓存fn(n)
  return function(n){
    if(obj[n]) return obj[n] 
    if(n===0) return 0
      else if(n===1) return 1
      else {
        res = fn(n-1) + fn(n-2)
        obj[n] = res
        return res
      }
  }
})()

【Javascript】手写运算符new创建实例并实现js继承

new 运算符

在js中,new()常被用来创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例
本文,主要讲如何手写function new创建实例并实现js继承

对于想彻底捋清楚new做了哪些操作的同学,可以查看我的这篇文章:

【Javascript】彻底捋清楚javascript中 new 运算符的实现

手写实现 new()

function subNew(){
  var obj = {}
 将父级的原型prototype指向子级的隐式原型__proto__
  obj.__proto__ = Parent.prototype
 //创建实例的时候传参
  var res = Parent.call(obj,...arguments)
 //当构造函数有return时,返回return值,没有则返回obj{}
  return typeof(res) === 'object'&&res || obj    
}

使用 subNew() 创建实例并对比new()

var obj1 = new Constructor()
var obj2 = subNew()
console.log(new Constructor(123),subNew(123))
console.log(new Constructor({a:123}),subNew({a:123}))
console.log(obj1.__proto__,obj2.__proto__,Constructor.prototype)


可以发现 我们已经成功创建了new的实例,并且obj1,obj2的__proto__都指向Constructor的prototype

现在我们来尝试使用subNew()实现js继承

(继承的底层实原理其实是通过this.fn=Parent&&this.fn(),调用父类函数,改变this指向的同时,也改变了函数的内部对象(即运行时上下文) )

function Parent(sex)  
{
  this.sex = sex   
}  
Parent.prototype.saySex=function(){
  console.log(this.sex)
}
function Child()  
{  
  Parent.apply(this,[...arguments])
}  
function subNew(){
  var obj = {}
  obj.__proto__ = Parent.prototype
  var res = Parent.call(obj,...arguments)
  return typeof(res) === 'object'&&res || obj 
}
Child.prototype = subNew()   // 等同于 Child.prototype = new Parent()
var c=new Child('男');
c.saySex();


Child 继承成功了!!!

如果觉得我的文章对你有帮助,欢迎star我的blog

【Javascript】useEffect 死循环原理分析

什么情况下会出现死循环

  1. props设置了默认值为复合类型,且被劫持
import { useEffect, useState } from 'react'
function Form(props){
 
  const [value, key] = useState('')
   const { data } = props
    


   return  <div>form</div>
}

【算法系列 - 剑指Offer】旋转数组的最小数字

题目描述

把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个非减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。

JS实现

function minNumberInRotateArray(rotateArray)
{
    // write code here
    if(rotateArray.length == 0)
    {
        return 0
    }
    return Math.min(...rotateArray)
}

【Nodejs】复制文件并获取进度

const fs = require('fs')

const input = 'test.mp4'
var file = fs.createReadStream(input);
var out = fs.createWriteStream('./test1.mp4');

let totalSize = fs.statSync( input ).size  // 通过 fs.statSync 获取文件大小
let curSize = 0

file.on('data',function(chunk){
  curSize += chunk.length
  const percent = (curSize / totalSize * 100)
  out.write(chunk)
  console.log(`读取中,当前进度:${percent}`)
});
file.on('end',function(){
	out.end();
})

Sketch 插件开发

相信大家都对Sketch有一定的了解和认识。除了基础的矢量设计功能以外,插件更是让Sketch保持强大的独门秘籍。Sketch开放了第三方插件接口,设计师可以在几百种的插件中轻松找到适合自己工作方式的插件,并且他们都非常容易获得和安装。这里主要介绍使用Javascript API for Sketch开发Sketch插件。

Sketch成为梦想中的“设计师工具箱”。但是每个人都有不同的需求,也许你需要一个我们还没有实现的功能。不要担心:插件已经可以满足您的需求,或者你可以轻松创建一个插件。

一、Sketch插件可以做什么?

Sketch中的插件可以做任何用户可以做的事情(甚至更多!)。例如:

根据复杂的规则选择文档中的图层

操作图层属性

创建新图层

以所有支持的格式导出资产

与用户交互(要求输入,显示输出)

从外部文件和Web服务获取数据

与剪贴板交互

操作Sketch的环境(编辑指南,缩放等...)

通过从插件调用菜单选项来自动化现有功能

设计规格

内容生成

透视转换

二、插件简介

Sketch 插件都是 *.sketchplugin 的形式,其实就是一个文件夹,通过右键显示包内容,可以看到最普通的内部结构式是这样的:

Sketch 插件内部结构

manifest.json用来声明插件配置信息,commands 定义所有可执行命令,每条 command 有唯一标志符,identifier,menu 定义插件菜单,通过 identifier 关联到执行命令。

my-commond.js是插件逻辑的实现代码实现文件。

三、Javascript API for Sketch

这是Sketch的原型Javascript API。 原生Javascript,Sketch的完整内部结构的一个易于理解的子集。它仍然是一项正在进行中的工作。

Javascript API for Sketch 原理:

Javascript API for Sketch 原理

四、开发文档

1、开发文档
https://developer.sketchapp.com/

2、API
https://developer.sketchapp.com/reference/api/

3、Action API
https://developer.sketchapp.com/guides/action-api/

https://developer.sketchapp.com/reference/action/

4、Sketch Source
https://github.com/BohemianCoding/SketchAPI/tree/develop/Source

5、Demo
https://github.com/BohemianCoding/SketchAPI/tree/develop/examples

五、Sketch webView

Sketch模块,用于使用webview创建复杂的UI。有别于一般的插件页面,可以使用webview模块加载一个复杂的Web应用,使其与Sketch进行交互。

1、BrowserWindow

在浏览器窗口中创建和控制Sketch:

// In the plugin.
const BrowserWindow = require('sketch-module-web-view');
const identifier = "identifier";//webview 标识

let win = new BrowserWindow({identifier, width: 800, height: 600, alwaysOnTop: true})
win.on('closed', () => {
win = null
})

// Load a remote URL
win.loadURL('https://github.com')

// Or load a local HTML file
win.loadURL(require('./index.html'))

2、获取已存在的BrowserWindow

import { getWebview } from 'sketch-module-web-view/remote';
const = identifier = "identifier";

const existingWebview = getWebview(identifier);
if (existingWebview) {
if (existingWebview.isVisible()) {
// close the devtool if it's open
existingWebview.close()
}
}

3、webContents

const BrowserWindow = require('sketch-module-web-view')

let win = new BrowserWindow({ width: 800, height: 1500 })
win.loadURL('http://github.com')

let contents = win.webContents
console.log(contents)

4、skech与webview的通信

1)Sending a message to the WebView from your plugin command

On the WebView:

window.someGlobalFunctionDefinedInTheWebview = function(arg) {
  console.log(arg)
}

On the plugin:

browserWindow.webContents
  .executeJavaScript('someGlobalFunctionDefinedInTheWebview("hello")')
  .then(res => {
    // do something with the result
  })

2)Sending a message to the plugin from the WebView

On the plugin:

var sketch = require('sketch')

browserWindow.webContents.on('nativeLog', function(s) {
sketch.UI.message(s)
})

On the webview:

window.postMessage('nativeLog', 'Called from the webview')

// you can pass any argument that can be stringified
window.postMessage('nativeLog', {
a: b,
})

// you can also pass multiple arguments
window.postMessage('nativeLog', 1, 2, 3)

六、构建开发工程

1、确立技术栈

使用Sketch webView的方式开发插件。用户通过操作插件界面,webview与Sketch通信解决用户的问题。这样插件界面可以使用现今所有的前端框架与组件库。

1)webView框架选择Umi + Ant Design

注:WebView框架也可以单独的工程与部署。

2)使用Sketch 官方skpm插件工程
3)调试工具

A、使用官方的sketch-dev-tools sketch内作为调试工具

下载代码,代码运行安装插件即可:

npm install
npm run build

调试界面如下:

Sketch Plugin调试界面

B、使用浏览器的开发者模式调试webView。

4)服务端技术方案

轻量级服务器部署方案 -(阿里云CenOS+宝塔)

2、构建工程

1)创建Sketch插件基础工程

首先,创建sketch-webview-kit插件工程:

npm install -g skpm
skpm create sketch-webview-kit //创建sketch-webview-kit插件工程

其次,依赖sketch-module-web-view:

npm install sketch-module-web-view
2)创建webView工程(Umi + Ant Design)

首先,创建webView工程目录,

$ mkdir webapp && cd webapp

然后,创建webView工程

yarn create umi

依次:
选择 app, 然后回车确认;

选上 antd 和 dva,然后回车确认;

最后,安装依赖:

$ yarn
3)配置webView工程

A.部署打包配置

.umirc.js文件中,添加:

outputPath:'../src/dist', //打包后的目录
exportStatic: {
  htmlSuffix: true,
    dynamicRoot: true //静态自由部署
},

B.HTML 模板

由于Umi生成没有Html文件,可以自己配置。新建 src/pages/document.ejs,umi 约定如果这个文件存在,会作为默认模板,内容上需要保证有 <div id="root"></div>,比如:

<!doctype html>
<html>
<head>
  <meta charset="utf-8" />
  <title>Your App</title>
</head>
<body>
  <div id="root"></div>
</body>
</html>

C.添加新页面

直接在pages文件夹下建立页面的js与css样式文件即可。

D.《基于 umi 的 React 项目结构介绍》

3、sketch加载webView工程与联调

1)sketch加载webView

第一种方法:

直接部署webView工程,通过Url加载:

win.loadURL('https://github.com')

第二种方法:

加载webView工程打包后的文件:

win.loadURL(require('./dist/index.html'))


注意:

此方法,由umi打包后的静态资源(css、js)需要拷贝到

pannel3/pannel3.sketchplugin/Contents/Resources/_webpack_resources

下。

12)联调加载方法:

本地启动webView工程,本地webView工程会在8000端口起一个服务,加载此服务即可:

const Panel = `http://localhost:8000#${Math.random()}`;
win.loadURL(Panel)

4、项目成果

文件目录如下:

sketch-webview-kit目录结构

七、拓展

1、 [React - SketchApp](https://github.com/airbnb/react-sketchapp

)
是一个开源库,为设计系统量身定制。它通过将 React 元素渲染到 Sketch 来连接设计和开发之间的鸿沟。

Sketch Javascript API 是源生代码,React - SketchApp 使用react对Javascript API 进行了二次封装。

1)API

2)Demo

【CSS】你可能不知道的Animation动画技巧与细节

引言

在web应用中,前端同学在实现动画效果时往往常用的几种方案:

  1. css3 transition / animation - 实现过渡动画
  2. setInterval / setTimeout - 通过设置一个间隔时间来不断的改变图像的位置
  3. requestAnimationFrame - 通过一个回调函数来改变图像位置,由系统来决定这个回调函数的执行时机,比定时修改的性能更好,不存在失帧现象

在大多数需求中,css3的 transition / animation 都能满足我们的需求,并且相对于js实现,可以大大提升我们的开发效率,降低开发成本。

本篇文章将着重对 animation 的使用做个总结,如果你的工作中动画需求较多,相信本篇文章能够让你有所收获:

  • Animation 常用动画属性
  • Animation 实现不间断播报
  • Animation 实现回弹效果
  • Animation 实现直播点赞效果 ❤️
  • Animation 与 Svg 又会擦出怎样的火花呢?
    1. Loading 组件
    2. 进度条组件
  • Animation steps() 运用⏰
    1. 实现打字效果
    2. 绘制帧动画

Animation 常用动画属性 参考资料

介绍完 animation 常用属性,为了将这些属性更好地理解与运用,下面将手把手实现一些DEMO具体讲述

Animation 实现不间断播报 Online Code

通过修改内容在父元素中的y轴的位置来实现广播效果

@keyframes scroll {   
  0%{
    transform: translate(0, 0);
  }
  100%{
    transform: translate(0, -$height);
  }
}

.ul {
  animation-name: scroll;
  animation-duration: 5s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
  /* animation: scroll 5s linear infinite; 动画属性简写 */
}

此处为了保存广播滚动效果的连贯性,防止滚动到最后一帧时没有内容,需要多添加一条重复数据进行填充

<div class="ul">
  <div class="li">小刘同学加入了凹凸实验室</div>
  <div class="li">小邓同学加入了凹凸实验室</div>
  <div class="li">小李同学加入了凹凸实验室</div>
  <div class="li">小王同学加入了凹凸实验室</div>
	<!--   插入用于填充的数据数据 -->
  <div class="li">小刘同学加入了凹凸实验室</div>  
</div>

Animation 实现回弹效果 Online Code

通过将过渡动画拆分为多个阶段,每个阶段的top属性停留在不同的位置来实现

/* 规定动画,改变top,opacity */
@keyframes animate {  
  0% {
    top: -100%;
    opacity: 0;
  }
  25% {
    top: 60;
    opacity: 1;
  }
  50% {
    top: 48%;
    opacity: 1;
  }
  75% {
    top: 52%;
    opacity: 1;
  }
  100%{
    top: 50%;
    opacity: 1;
  }
}

为了让过渡效果更自然,这里通过 cubic-bezier() 函数定义一个贝塞尔曲线来控制动画播放速度

过渡动画执行完后,为了将让元素应用动画最后一帧的属性值,我们需要使用 animation-fill-mode: forwards

.popup {
  animation-name: animate;
  animation-duration: 0.5s;
  animation-timing-function: cubic-bezier(0.21, 0.85, 1, 1);
  animation-iteration-count: 1;
  animation-fill-mode: forwards;
  /* animation: animate 0.5s cubic-bezier(0.21, 0.85, 1, 1) 1 forwards; 动画属性简写 */
}

Animation 实现点赞效果 Online Code

相信大多数同学都知道点赞效果,本文章会实现一个简易版的点赞效果,主要讲述一下实现思路:

1.为了让气泡可以向上偏移,我们需要先实现一个y轴方向上移动的 @Keyframes 动画

/* 规定动画,改变y轴偏移距离*/
@keyframes animation-y { 
  0%{
   transform:  translate(-50%, 100px) scale(0);
  }
  50%{
   transform:  translate(-50%, -100px) scale(1.5);
  }
  100%{
    transform:  translate(-50%, -300px) scale(1.5);
  }
}

2.为了让气泡向上偏移时显得不太单调,我们可以再实现一个x轴方向上移动的 @Keyframes 动画

/* 规定动画,改变x轴偏移距离 */
@keyframes animation-x {
  0%{
    margin-left: 0px;
  }
  25%{
    margin-left: 25px;
  }
  75%{
    margin-left: -25px;
  }
  100%{
    margin-left: 0px;
  }
}

这里我理解:

  • 虽然是修改 margin 来改变x轴偏移距离,但实际上与修改 transform 没有太大的性能差异
  • 因为通过 @keyframes animation-y 中的 transform 已经新建了一个渲染层 ( PaintLayers )
  • animation 属性 可以让该渲染层提升至 合成层(Compositing Layers) 拥有单独的图形层 ( GraphicsLayer ) ,与开启了硬件加速的效果一样 ,不会影响其他渲染层的 paint、layout
  • 相关参考文档 - 无线性能优化:Composite
  • 如下图所示:

如笔者这里理解有误,还请读者大佬指出,感激不尽~

3.给气泡应用上我们所实现的两个 @Keyframes 动画

.bubble {
  animation: animation-x 3s -2s linear infinite,animation-y 4s 0s linear 1;
/*  给 bubble 开启了硬件加速 */
}

4.在点赞事件中,通过 js 操作动态添加/移除气泡元素

function like() {
  const likeDom = document.createElement('div');
  likeDom.className = 'bubble'; // 添加样式
  document.body.appendChild(likeDom);  // 添加元素
  setTimeout( () => {
    document.body.removeChild(likeDom);  // 移除元素
  }, 4000)
}

Animation 与 Svg 绘制 loading/进度条 组件 Online Code

1.首先,我们使用 svg 绘制一个圆周长为2 * 25 * PI = 157 的圆

<svg with='200' height='200' viewBox="0 0 100 100"  >
  <circle cx="50" cy="50" r="25"  fill="transparent" stroke-width="4" stroke="#0079f5" ></circie>
</svg>

2.将实线圆绘制成虚线圆,这里需要用 stoke-dasharray:50, 50 (可简写为50) 属性来绘制虚线, stoke-dasharray 参考资料

  • 它的值是一个数列,数与数之间用逗号或者空白隔开,指定短划线(50px)缺口(50px)的长度。
  • 由于50(短划线) + 50(缺口) + 50(段划线) = 150, 150 < 157,无法绘制出完整的圆,所以会导致右边存在缺口(7px)
<svg with='200' height='200' viewBox="0 0 100 100"  >
  <circle cx="50" cy="50" r="25"  fill="transparent" stroke-width="4" stroke-dasharray="50" stroke="#0079f5" ></circie>
</svg>

3.stroke-dashoffset 属性可以使圆的短划线和缺口产生偏移,添加 @Keyframes 动画后能够实现从无到有的效果,stoke-dashoffset参考资料

  • 设置 stroke-dasharray="157 157",指定 短划线(157px)缺口(157px) 的长度。
  • 添加 @Keyframes 动画 修改stroke-dashoffset值, 值为正数逆时针偏移🔄,, 值为负数时,顺时针偏移🔃
@keyframes loading {
  0%{
    stroke-dashoffset: 0;
  }
  100%{
    stroke-dashoffset: -157; /* 线条顺时针偏移 */
  }
}
circle{
    animation: loading 1s 0s ease-out infinite;
}

4.修改短划线和缺口值

  • 为了让 loading 组件线条可见,我们需要一个50px的短划线,设置 stroke-dasharray="50"
  • 为了让短划线发生偏移后可以完全消失,缺口需要大于或等于圆周长157,设置 stroke-dasharray="50 157"
  • 添加 @Keyframes 动画,为了让动画结束时仍处理动画开始位置,需要修改 stroke-dashoffset:-207(短划线+缺口长度)
  • 进度条也是类似原理,帮助理解 stroke-dashoffset 属性,具体实现请查看示例
@keyframes loading {
  0%{
    stroke-dashoffset: 0;
  }
  100%{
    stroke-dashoffset: -207; /* 保证动画结束时仍处理动画开始位置 */
  }
}
circle{
    animation: loading 1s 0s ease-out infinite;
}

Animation steps()运用

steps()animation-timing-function 的属性值

animation-timing-function : steps(number[, end | start])
  • steps 函数指定了一个阶跃函数,它接受两个参数
  • 第一个参数接受一个整数值,表示两个关键帧之间分几步完成
  • 第二个参数有两个值 start or end 。默认值为 end
  • step-start 等同于 step(1, start)。step-end 等同于 step(1, end)

steps 适用于关键帧动画,第一个参数将两个关键帧细分为N帧,第二个参数决定从一帧到另一帧的中间间隔是用开始帧还是结束帧来进行填充。

看下图可以发现:

  • steps(N, start)将动画分为N段,动画在每一段的起点发生阶跃(即图中的空心圆 → 实心圆),动画结束时停留在了第N帧
  • steps(N, end)将动画分为N段,动画在每一段的终点发生阶跃(即图中的空心圆 → 实心圆),动画结束时第N帧已经被跳过(即图中的空心圆 → 实心圆),停留在了N+1帧。


steps()参考资料

实践出真知!

Animation 实现打字效果 Online Code

  • 此处用英文字母(I'm an O2man.)举例,一共有13个字符。[经测试,多数中文字体每个字符宽高都相等]
  • steps(13)可以将 @Keyframes 动画分为13阶段运行,且每一阶段运行距离相等

效果如下:

/* 改变容器宽度 */
@keyframes animate-x { 
  0%{
    width: 0;
  }
}

p { 
    width: 125px;
    overflow: hidden;
    border-right: 1px solid transparent;
    animation: animate-x 3s 0s steps(13) 1 forwards;   
}
  • 可以发现仅仅这样还不够,动画运行过程中出现了字符被截断的情况,为了保证每个阶段运行后能准确无误地显示当前所处阶段的字符,我们还需要保证每个字符的width与动画每一阶段运行的距离相等
  • 设置Monaco字体属性,用以保证每个字符的 width 相同,具体像素受fontSize属性影响,示例中的字体宽度约为 9.6px,9.6px * 13(段数) = 124.8px (125px) ,所以当我们设置容器宽度为 125px,即可的达成目的:每个字符的 width 与动画每一阶段运行的距离相等(约为 9.6px )
p { 
    /* 设置 Monaco 字体属性,字体大小为16px,用以保证每个字符的 width 相同,width 约为9.6p */
    font-family: Monaco; 
    /* 9.6px * 13 = 124.8px (125px) */
    width: 125px ;
    font-size: 16px;
    overflow: hidden;
    border-right: 1px solid transparent;
    /* 同时应用动画 animate-x、cursor-x */
    animation: animate-x 3s 0s steps(13) 1 forwards,cursor-x 0.4s 0s linear infinite;  
}

Animation 实现帧动画 ⏰ Online Code

.main {
  width: 260px;
  height: 200px;
  background: url(url) no-repeat;
  background-size: 100%;
  background-position: 0 0;
}
  • 添加 @Keyframes 修改 background-position ,让背景图移动
@keyframes animate { 
    0% {
        background-position: 0 0;
    }

    100% {
        background-position: 0 100%;
    }
}
.main{
  width: 260px;
  height: 200px;
  background: url(url) no-repeat;
  background-size: 100%;
  background-position: 0 0;
  animation: animate 2s 1s steps(47) infinite alternate;
}
  • 同时, css 还提供了animation-play-state用于控制动画是否暂停
input:checked+.main{
    animation-play-state: paused;
}

文章篇幅较长,感谢大家的阅读,希望各位看客能够有所收获~ ~ ~

如果这篇文章对你有帮助,欢迎关注我的博客


参考资料

CSS 参考手册
SVG学习之stroke-dasharray 和 stroke-dashoffset 详解
理解CSS3 Animation中的steps()
【译】css动画里的steps()用法详解
CSS Will Change
无线性能优化:Composite

【Javascript】实现一个简易版react-redux

index.js (createStore)

import React, { Component } from 'react'
import PropTypes from 'prop-types'
import ReactDOM from 'react-dom'
import Header from './Header'
import Content from './Content'
import './index.css'
import { Provider } from './connect'


function createStore (reducer) {
  let state = null
  const listeners = []
  const subscribe = (listener) => listeners.push(listener)
  const getState = () => state
  const dispatch = (action) => {
    state = reducer(state, action)
    listeners.forEach((listener) => listener())
  }
  dispatch({}) // 初始化 state
  return { getState, dispatch, subscribe }
}

const themeReducer = (state, action) => {
  if (!state) return {
    themeColor: 'red'
  }
  switch (action.type) {
    case 'CHANGE_COLOR':
      return { ...state, themeColor: action.themeColor }
    default:
      return state
  }
}

const store = createStore(themeReducer)

class Index extends Component {
  // static childContextTypes = {
  //   store: PropTypes.object
  // }

  // getChildContext () {
  //   return { store }
  // }
  render () {
    return (
      <div>
        <Header />
        <Content />
      </div>
    )
  }
}

ReactDOM.render(
  <Provider store={store}>
    <Index />
  </Provider>,
  document.getElementById('title')
)

connect.js

import React, { Component } from 'react'
import PropTypes from 'prop-types'

export const connect = (mapStateToProps,mapDispatchToProps) => (WrappedComponent) => {
  class Connect extends Component {
    static contextTypes = {
      store: PropTypes.object
    }

    constructor () {
      super()
      this.state = {
        allProps: {}
      }
    }

    componentWillMount () {
      const { store } = this.context
      this._updateProps()
      store.subscribe(() => this._updateProps())
    }

    _updateProps () {
      const { store } = this.context
      let stateProps = mapStateToProps
        ? mapStateToProps(store.getState(), this.props)
        : {} // 防止 mapStateToProps 没有传入
      let dispatchProps = mapDispatchToProps
        ? mapDispatchToProps(store.dispatch, this.props)
        : {} // 防止 mapDispatchToProps 没有传入
      this.setState({
        allProps: {
          ...stateProps,
          ...dispatchProps,
          ...this.props
        }
      })
    }

    render () {
      const { store } = this.context
      let stateProps = mapStateToProps(store.getState())
      // {...stateProps} 意思是把这个对象里面的属性全部通过 `props` 方式传递进去
      return <WrappedComponent {...this.state.allProps} />
    }
  }

  return Connect
}


export class Provider extends Component {
  static propTypes = {
    store: PropTypes.object,
    children: PropTypes.any
  }

  static childContextTypes = {
    store: PropTypes.object
  }

  getChildContext () {
    return {
      store: this.props.store
    }
  }

  render () {
    return (
      <div>{this.props.children}</div>
    )
  }
}

themeswitch.js

import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from './connect'


class ThemeSwitch extends Component {
  static contextTypes = {
    store: PropTypes.object
  }

  constructor () {
    super()
    this.state = { themeColor: '' }
  }

  // componentWillMount () {
  //   const { store } = this.context
  //   this._updateThemeColor()
  //   store.subscribe(() => this._updateThemeColor())
  // } 

  // _updateThemeColor () {
  //   const { store } = this.context
  //   const state = store.getState()
  //   this.setState({ themeColor: state.themeColor })
  // }

  handleSwitchColor (color) {
    const { store } = this.context
    if (this.props.onSwitchColor) {
      this.props.onSwitchColor(color)
    }
  }
  render () {
    return (
      <div>
        <button onClick={this.handleSwitchColor.bind(this, 'red')} style={{color:this.state.themeColor}}>Red</button>
        <button onClick={this.handleSwitchColor.bind(this, 'blue')} >Blue</button>
      </div>
    )
  }
}

const mapStateToProps = (state) => {
  return {
    themeColor: state.themeColor
  }
}
const mapDispatchToProps = (dispatch) => {
  return {
    onSwitchColor: (color) => {
      dispatch({ type: 'CHANGE_COLOR', themeColor: color })
    }
  }
}
ThemeSwitch = connect(mapStateToProps, mapDispatchToProps)(ThemeSwitch)

export default ThemeSwitch

【算法系列 - 剑指Offer】实现队列

题目1:

实现1个队列

JS实现

const stack1 = []
function push(node)
{
    stack1.push(node)
    // write code here
}
function pop()
{
    return stack1.shift()
    // write code here
}

题目2:

用两个栈实现1个队列

JS实现

var stack1 = []
var stack2 = []
function push(node)
{
    stack1.push(node)
    // write code here
}
function pop()
{
    while(stack1.length>1)
    {
         stack2.push(stack1.pop())
    }
    var res = stack1[0]
    stack1 = stack2.reverse()
    stack2 = []
    return res
    // write code here
}

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.