Lucky_LYZ

Lucky_LYZ 查看完整档案

北京编辑  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

Lucky_LYZ 收藏了文章 · 3月21日

React项目从Javascript到Typescript的迁移经验总结

抛转引用

现在越来越多的项目放弃了javascript,而选择拥抱了typescript,就比如我们熟知的ant-design就是其中之一。面对越来越火的typescript,我们公司今年也逐渐开始拥抱typescript。至于为什么要使用typescript?本文不做深入探讨,对这方面有兴趣的小伙伴们可以去看一下这篇文章:

TypeScript体系调研报告

这篇文章比较全面地介绍了TypeScript,并且和Javascript做了一个对比。看完上面这篇文章,你会对TypeScript有一个比较深入的认识,另外在TypeScript和Javascript的取舍上,可以拿捏得更好。

开始迁移

在开始迁移之前,我要说点题外话,本篇文章仅是记录我在迁移过程中遇到的问题以及我是如何解决的,并不会涉及typescript的教学。所以大家在阅读本篇文章之前,一定要对typescript有一个基础的认识,不然你读起来会非常费力。

环境调整

由于Typescript是Javascript的超集,它的很多语法浏览器是不能识别的,因此它不能直接运行在浏览器上,需要将其编译成JavaScript才能运行在浏览器上,这点跟ES6需要经过babel编译才能支持更多低版本的浏览器是一个道理。

tsconfig.json

首先我们得装一个typescript,这就跟我们在用babel前需要先装一个babel-core是一个道理。

yarn global add typescript 
yarn add typescript 

有些人会选择将typescript安装在全局环境上,但是我个人建议是装在项目目录下的,因为每个项目的typescript版本是不完全一样的,装在全局容易因为版本不同而出现问题。如果需要用tsc命令的话,可以借助npx去实现。接下来我们执行如下命令生成tsconfig.json,这玩意就跟.babelrc是一个性质的。

npx tsc --init

执行完之后,你的项目根目录下便会有一个tsconfig.json这么一个东西,但是里面会有很多注释,我们先不用管他的。

webpack

安装ts-loader用于处理ts和tsx文件,类似于babel-loader。

yarn add ts-loader -D

相应的webpack需要加上ts的loader规则:

module.exports = {
    //省略部分代码...
    module: {
        rules: [
            {
                test:/\.tsx?$/,
                loader:'ts-loader'
            }
            //省略部分代码...
        ]
    }
    //...省略部分代码
}

之前用javascript的时候,可能有人不使用.jsx文件,整个项目都是用的.js文件,webapck里面甚至都不配.jsx的规则。但是在typescript项目中想要全部使用.ts文件这就行不通了,会报错,所以当用到了jsx的用法的时候,还是得乖乖用.tsx文件,因此这里我加入了.tsx的规则。

删除babel

关于babel这块,网上有不少人是选择留着的,理由很简单,说是为了防止以后会使用到JavaScript,但是我个人觉得是没有必要留着babel。因为我们整个项目里面基本上只有使用第三方包的时候才会用到javascript,而这些第三方包基本上都是已经编译成了es5的代码了,不需要babel再去处理一下。而业务逻辑里面用javascript更是不太可能了,因为这便失去了使用typescript的意义。综上所述,我个人觉得是要删除babel相关的东西,降低项目复杂度。但是有一个例外情况:。

当你用了某些babel插件,而这些插件的功能恰巧是typescript无法提供的,那你可以保留babel,并且与typescript结合。

文件名调整

整个src目下所有的.js结尾的文件都要修改文件名,使用到jsx语法的就改成.tsx文件,未使用的就改成.ts文件,这块工作量比较大,会比较头疼。另外改完之后文件肯定会有很多标红的地方,不要急着去改它,后面我们分类统一去改。

解决报错

webpack入口文件找不到


由于我们在做文件名调整的时候,把main.js改成main.tsx,因此webpack的入口文件要改成main.tsx。

module.exports = {
    //省略部分代码...
    entry: {
        app: './src/main.tsx'
    },
    //省略部分代码...
}

提示不能使用jsx的语法


这个解决很简单,去tsconfig配置一下即可。

{
   "compilerOptions":{
        "jsx": "react"
   }
}

jsx这个配置项有三个值可选择,分别是"preserve","react-native"和"react"。在preservereact-native模式下生成代码中会保留JSX以供后续的转换操作使用(比如:Babel)。另外,preserve输出文件会带有.jsx扩展名,而react-native是.js拓展名。react模式会生成React.createElement,在使用前不需要再进行转换操作了,输出文件的扩展名为.js。

模式输入输出输出文件扩展名
preserve<div /><div />.jsx
react<div />React.createElement("div").js
react-native<div /><div />.js

webpack里面配置的alias无法解析

module.exports = {
    //省略部分代码...
    resolve: {
        alias:{
          '@':path.join(__dirname,'../src')
        }
        //省略部分代码...    
    },
    //省略部分代码...   
}


这里需要我们额外在tsconfig.json配置一下。

{
    "compilerOptions":{
        "baseUrl": ".",
        "paths": {
          "@/*":["./src/*"]
        } 
    }
}

具体如何配置,请看typescript的文档,我就不展开介绍了,但是要注意的是baseUrl和paths一定要配合使用。

https://www.tslang.cn/docs/ha...

无法自动添加拓展名而导致找不到对应的模块


原先我们在webpack里是这么配置的:

module.exports = {
    //省略部分代码... 
    resolve: {
        //省略部分代码... 
        extensions: ['.js', '.jsx', '.json']
    },
    //省略部分代码... 
}

但是我们项目里所有.js和.jsx的文件都改成了.ts和.tsx文件,因此配置需要调整。

{
    //省略部分代码... 
    resolve: {
        //省略部分代码... 
        extensions: ['.ts','.tsx','.js', '.jsx', '.json']
    },
    //省略部分代码... 
}

Could not find a declaration file for module '**'

这个比较简单,它提示找不到哪个模块的声明文件,你就装个哪个模块的就好了,安装格式如下:

yarn add @types/**

举个🌰,如果提示Could not find a declaration file for module 'react',那你应该执行如下命令:

yarn add @types/react

这个仅限于第三方包,如果是项目自己的模块提示缺少声明文件,那就需要你自己写对应的声明文件了。比如你在window这个全局对象上挂载了一个对象,如果需要使用它的话,就需要做一下声明,否则就会报错。至于具体怎么写,这得看typescript的文档,这里就不展开说明了。

https://www.tslang.cn/docs/ha...

Cannot find type definition file for '**'


这些并没有在我们的业务代码里直接用到,而是第三方包用到的,遇到这种情况,需要检查一下tsconfig.json中的typeRoots这个配置项有没有配置错误。一般来说是不用配置typeRoots,但是如果需要加入额外的声明文件路径,就需要对其进行修改。typeRoots是有一个默认值,有人会误以为这个默认值是“["node_modules"]”,因此会有人这样配置:

{
    "compilerOptions":{
        "typeRoots":["node_modules",...,"./src/types"]
    }
}

实际上typeRoots的默认值“["@types"]”,所有可见的"@types"包都会在编辑过程中被加载进来,比如“./node_modules/@types/”,“../node_modules/@types/”和“../../node_modules/@types/”等等都会被加载进来。所以遇到这种问题,你的配置应该改成:

{
    "compilerOptions":{
        "typeRoots":["@types",...,"./src/types"]
    }
}

在实际项目中,@types基本上存在于根目录下的node_modules下,因此这里你可以改成这样:

{
    "compilerOptions":{
        "typeRoots":["node_modules/@types",...,"./src/types"]
    }
}

不支持decorators(装饰器)


typescript默认是关闭实验性的ES装饰器,所以需要在tsconfig.json中开启。

{
    "compilerOptions":{
        "experimentalDecorators":true
    }
}

Module '**' has no default export


提示模块代码里没有“export
default”,而你却用“import from ”这种默认导入的形式。对于这个问题,我们需要把tsconfig.json配置项“allowSyntheticDefaultImports”设置为true。允许从没有设置默认导出的模块中默认导入。不过不必担心会对代码产生什么影响,这个仅仅为了类型检查。

{
    "compilerOptions":{
        "allowSyntheticDefaultImports":true
    }
}

当然你也可以使用“esModuleInterop”这个配置项,将其设置为true,根据“allowSyntheticDefaultImports”的默认值,如下:

module === "system" or --esModuleInterop

对于“esModuleInterop”这个配置项的作用主要有两点:

  • 提供__importStar和__importDefault两个helper来兼容babel生态
  • 开启allowSyntheticDefaultImports

对于“esModuleInterop”和“allowSyntheticDefaultImports”选用上,如果需要typescript结合babel,毫无疑问选“esModuleInterop”,否则的话,个人习惯选用“allowSyntheticDefaultImports”,比较喜欢需要啥用啥。当然“esModuleInterop”是最保险的选项,如果对此拿捏不准的话,那就乖乖地用“esModuleInterop”。

无法识别document和window这种全局对象


遇到这种情况,需要我们在tsconfig.json中lib这个配置项加入一个dom库,如下:

{
    "compilerOptions":{
        "lib":[
            "dom",
            ...,
            "esNext"
        ]
    }
}

文件中的标红问题

关于这个问题,我们需要分两种情况来考虑,第一种是.ts的文件,第二种是.tsx文件。下面来看一下具体是哪些注意的点(Ps:以下提到的注意的点并不能完全解决文件中标红的问题,但是可以解决大部分标红的问题):

第一种:.ts文件

这种文件在你的项目比较少,比较容易处理,根据实际情况去加一下类型限制,没有特别需要讲的。

第二种:.tsx文件

这种情况都是react组件了,而react组件又分为无状态组件和有状态组件组件,所以我们分开来看。

无状态组件

对于无状态组件,首先得限制他是一个FunctionComponent(函数组件),其次限制其props类型。举个🌰:

import React, { FunctionComponent, ReactElement } from 'react';
import {LoadingComponentProps} from 'react-loadable';
import './style.scss';

interface LoadingProps extends LoadingComponentProps{
  loading:boolean,
  children?:ReactElement
}

const Loading:FunctionComponent<LoadingProps> = ({loading=true,children})=>{
  return (
    loading?<div className="comp-loading">
      <div className="item-1"></div>
      <div className="item-2"></div>
      <div className="item-3"></div>
      <div className="item-4"></div>
      <div className="item-5"></div>
    </div>:children
  )  
}
export default Loading;

其中你要是觉得FunctionComponent这个名字比较长,你可以选择用类型别名“SFC”或者“FC”。

有状态组件

对于有状态组件,主要注意三点:

  1. props和state都要做类型限制
  2. state用readonly限制“this.state=**”的操作
  3. 对event对象做类型限制
import React,{MouseEvent} from "react";
interface TeachersProps{
  user:User
}
interface TeachersState{
  pageNo:number,
  pageSize:number,
  total:number,
  teacherList:{
    id: number,
    name: string,
    age: number,
    sex: number,
    tel: string,
    email: string
  }[]
}
export default class Teachers extends React.PureComponent<TeachersProps,TeachersState> {
    readonly state = {
        pageNo:1,
        pageSize:20,
        total:0,
        userList:[]
    }
    handleClick=(e:MouseEvent<HTMLDivElement>)=>{
        console.log(e.target);
    }
    //...省略部分代码
    render(){
        return <div onClick={this.handleClick}>点击我</div>
    }
}

实际项目里,组件的state可能会有很多值,如果按照我们上面这种方式去写会比较麻烦,所以可以考虑一下下面这个简便写法:

import React,{MouseEvent} from "react";
interface TeachersProps{
  user:User
}
const initialState = {
  pageNo:1,
  pageSize:20,
  total:0,
  teacherList:[]
}
type TeachersState = Readonly<typeof initialState>
export default class Teachers extends React.PureComponent<TeachersProps,TeachersState> {
    readonly state = initialState
    handleClick=(e:MouseEvent<HTMLDivElement>)=>{
        console.log(e.target);
    }
    //...省略部分代码
    render(){
        return <div onClick={this.handleClick}>点击我</div>
    }
}

这种写法会简便很多代码,但是类型限制效果上明显不如第一种,所以这种方法仅仅作为参考,可根据实际情况去选择。

Ant Design丢失样式文件

当我们把项目启动起来之后,某些同学的页面可能会出现样式丢失的情况,如下:

打开控制台,我们发现Ant Design的类名都找不到对应的样式:


出现这种情况是因为我们把babel删除之后,用来按需加载组件样式文件的babel插件babel-plugin-import也随着丢失了。不过typescript社区有一个babel-plugin-import的Typescript版本,叫做“ts-import-plugin”,我们先来安装一下:

yarn add ts-import-plugin -D

这个插件需要结合ts-loader使用,所以webpack配置中需要做如下调整:

const tsImportPluginFactory = require('ts-import-plugin')
module.exports = {
    //省略部分代码...
    module:{
        rules:[{
            test: /\.tsx?$/,
            loader: "ts-loader",
            options: {
                transpileOnly: true,//(可选)
                getCustomTransformers: () => ({
                  before: [
                    tsImportPluginFactory({
                        libraryDirectory: 'es',
                        libraryName: 'antd',
                        style: true
                    })
                  ]
                })
            }
        }]
    }
    //省略部分代码...
}

这里要注意一下transpileOnly: true这个配置,这是个可选配置,我建议是只有大项目中才加这个配置,小项目就没有必要了。由于typescript的语义检查器会在每次编译的时候检查所有文件,因此当项目很大的时候,编译时间会很长。解决这个问题的最简单的方法就是用transpileOnly: true这个配置去关闭typescript的语义检查,但是这样做的代价就是失去了类型检查以及声明文件的导出,所以除非在大项目中为了提升编译效率,否则不建议加这个配置。

配置完成之后,你的浏览器控制台可能会报出类似下面这个错误:

出现这个原因是因为你的typescript配置文件tsconfig.json中的module参数设置不对,两种情况会导致这个问题:

  • module设置成了“commonjs”
  • target设置"ES5"但是并未设置module(当target不为“ES6”时,module默认为“commonjs”)

解决这个办法就是把module设置为“esNext”便可解决这个问题。

{
    "compilerOptions":{
        "module":"esNext"
    }
}

可能会有小伙们说设置成“ES6”或者“ES2015”也是可以的,至于我为什么选择“esNext”而不是“ES6”或者“ES2015”,主要原因是设置成“ES6”或者“ES2015”之后,就不能动态导入了,因为项目使用了react-loadable这个包,要是设置成“ES6”或者“ES2015”的话,会报如下这个错误:

typescript提示我们需要设置成“commonjs”或者“ESNext”才可动态导入,所以保险起见,我是建议大家设置成ESNext。完成之后我们的页面就可以正常显示了。

说到module参数,这里要再多提一嘴说一下moduleResolution这个参数,它决定着typescript如何处理模块。当我们把module设置成“esNext”时,是可以不用管moduleResolution这个参数,但是大家项目里要是设置成“ES6”的话,那就要设置一下了。先看一下moduleResolution默认规则:

module === "AMD" or "System" or "ES6" ? "Classic" : "Node"

当我们module设置为“ES6”时,此时moduleResolution默认是“Classic”,而我们需要的是“Node”。为什么要选择“node”,主要是因为node的模块解析规则更符合我们要求,解析速度会更快,至于详情的介绍,可以参考Typescript的文档。

https://www.tslang.cn/docs/ha...

同样为了保险起见,我是建议大家强行将moduleResolution设置为“node”。

总结

以上就是我自己在迁移过程中遇到的问题,可能无法覆盖大家在迁移过程中所遇到的问题,如果出现我上面没有涉及的报错,欢迎大家在评论区告诉我,我会尽可能地完善这篇文章。最后再强调一下,本篇文章仅仅只是介绍了我个人在迁移至typescript的经验总结,并未完全覆盖tsconfig.json的所有配置项,文章未涉及到的配置项,还需大家多花点时间看看typescript的文档。最后附上我已迁移到typescript的项目的地址:

项目地址:https://github.com/ruichengpi...
查看原文

Lucky_LYZ 收藏了文章 · 3月3日

实用webpack插件之ProvidePlugin

ProvidePlugin.png

现代化前端的全局引入是一个很有趣的东西。
先来看下以下几个场景:

  • 在webpack中,我们可以在resolve的alias中定义一个层级较高的目录为一个自定义变量。例如resolve: { alias: { '@': path.join(__dirname, '..', 'src') }}
  • 在webpack中,我们也可以通过DefinePlugin将配置文件按照环境变量进行区分,高效的完成配置文件的按环境引入,无论是开发构建还是生产构建,都十分有用。
  • 在vue中,我们可以将一个常用的方法或者库定义在Vue.ptototye上,可以通过直写属性,也可以通过vue中的plugin install上去。例如Vue.prototype.$_ = lodash,在应用了vue的应用上下文中,可以通过this.$_获得对lodash的引用。
  • 在vue中,还有mixins,inject以及vuex等等这些全局绑定或者叫混入、注入方式的全局引入的实现。

来思考一个问题:

如果我们再Vue.prototype上绑定了太多,太大的第三方库,会不会导致root vue过大?
答案是肯定的。

有没有办法解决这个问题?
你可能会说,我不用this.xxx。用到的vue单文件组件直接import或者require就好了。

如果数以百计,数以千计甚至是数以万计的.vue文件中用到了呢?一直引入吗?
可以一直引入。但是会造成不必要的工作量。

有没有更加优雅的解决办法?

再来思考一个问题:

如果我要在一个webpack打包覆盖的地方的xxx.js文件中用到lodash,该怎么做?
通常来讲,我们会直接`import _ from' lodash'`或者`const _ = require('lodash')`。

如果和.vue一样,有很多很多js文件需要引入呢?一直引入吗?
可以一直引入。同样会造成不必要的工作量。

有没有更加优雅的实现方式?

看一张一直引入moment,引了99次的图先来感受一下:


虽然我的项目中是在优化moment的引入,但是为了直观明了,我将以引入lodash为例。

  • 使用ProvidePlugin的三种方式
  • 为何一直引入造成不必要工作量
  • 使用ProvidePlugin引入实践

    • webpack的plugins中增加$_的配置
    • eslint的globals增加$_的配置
    • 在Vue中如何使用$_
    • 在Vue的template中使用的注意事项

      • 为什么这个是最推荐的呢?
      • 那为什么不挂载到data上呢?
  • 思考

    • 使用ProvidePlugin后会比一直引入减小打包体积吗?
    • 使用ProvidePlugin有哪些注意事项?
    • 注入的ProvidePlugin是一个什么东西?

使用ProvidePlugin的三种方式

// 语法
new webpack.ProvidePlugin({
  identifier: 'module1',
  // identifier: ['module1', 'property1'],
});
  • module.exports

    • 直接引入
    • 引入某个函数
  • export default

module.exports

直接引入
new webpack.ProvidePlugin({
  $_: 'lodash',
});
引入某个函数
new webpack.ProvidePlugin({
  $_uniqBy: ['lodash','uniqBy']
});

export default

new webpack.ProvidePlugin({
  Vue: ['vue/dist/vue.esm.js', 'default']
});

为何一直引入造成不必要工作量

加入我们有a~z,a.js到z.js总结26个js文件,每个文件都需要引入lodash。

// a.js
import $_ from 'lodash';
// b.js
import $_ from 'lodash';
// c.js
import $_ from 'lodash';
// d.js
import $_ from 'lodash';
// e.js
import $_ from 'lodash';
// f.js
import $_ from 'lodash';
...
// z.js
import $_ from 'lodash';

这样做有以下几个弊端

  • 要乖乖引入26次
  • import进来之后的自定义名称可能会不统一,导致全局搜索困难

比如说下面这种场景,对于代码可读性是很不好的。

// a.js
import $_ from 'lodash';
// b.js
import _ from 'lodash';

使用ProvidePlugin引入实践

  • webpack的plugins中增加$_的配置
  • eslint的globals增加$_的配置
  • 在Vue中如何使用$_
  • 在Vue的template中使用的注意事项

webpack的plugins中增加$_的配置

// webpack.base.config.js
plugins: [
    new webpack.ProvidePlugin({
      $_: 'lodash',
    }),
],

eslint的globals增加$_的配置

// .eslintrc.js
globals: {
    $_: 'readonly', // 或者true
},

配置为readonly是因为我们不会改写lodash,仅仅是调用其方法。

在Vue中如何使用$_

假设在a.js中。
删除单独的lodash引入 :import from 'lodash'
script中直接使用$_ :$_.uniqBy(...)
template的使用事项可以看下文。

在Vue的template中使用的注意事项

ProvidePlugin注入的全局变量,在script中是完全没有问题的,但是在template中使用时会有一些小问题。

例如下面这样:

<p>{{$_(...)}</p>
data() {
    return {
         $_,
    }
}

遇到这种情况是什么原因呢?
Vue的模板语法中,不支持直接对以$或者_开头的自定义data属性,目的是避免与Vue的内部冲突。

[Vue warn]: 
Property "$_" must be accessed with "$data.$_".
Because properties starting with "$" or "_" are not proxied in the Vue instance to prevent conflicts with Vue internals

有以下几种方式解决这个问题:

  • 通过$data.$_访问
  • data中重命名后绑定
  • methods中绑定(最推荐)
通过$data.$_访问
<p>{{$data.$_(...)}</p>
data中重命名后绑定
<p>{{globalLodash(...)}</p>
data() {
    return {
          globalLodash: $_,
    }
}
methods中绑定(最推荐)
<p>{{$_(...)}</p>
methods: {
    $_
}
为什么这个是最推荐的呢?

这是因为ProvidePlugin最终返回给我们的,是一个hooks函数。

hooks () {
     return hookCallback.apply(null, arguments);
}

既然是一个函数,那么它其实就是一个method。
由于需要在vue的template中使用,所以需要将其挂载到vue实例上。
因此直接在methods中绑定,挂载到vue示例。

那为什么不挂载到data上呢?

避免额外的无用的开销。
这是因为data是用来定义一些响应式的数据的,我们的$_只是一个工具函数,不会有双向绑定的事情发生在它身上,因此也不需要定义在data中,vue不用为其定义单独的watcher,dep,getter,setter等等。

思考

注入的ProvidePlugin是一个什么东西?

是一个hooks函数。

hooks () {
     return hookCallback.apply(null, arguments);
}

使用ProvidePlugin后会比一直引入减小打包体积吗?

不会。
反而会略微增大一些,0.0X KB。
这是我自己对比使用ProvidePlugin前使用ProvidePlugin后打包文件体积大小得出的结论。

使用ProvidePlugin有哪些注意事项?

这些注意事项其实主要是为了增强代码可读性和可维护性。

  • 尽量定义出唯一性高的全局变量,例如$_,$moment
  • 同一个前端小组的成员都采用全局变量的方式引入
  • 最好是能维护一个全局变量的文档,在新人入职时特殊强调

看到这里,文章开头Vue.prototype.xxx和import和require重复引入的问题”有没有更加优雅的实现方式?“就迎刃而解啦。

快到你的项目中试试ProvidePlugin吧~

期待和大家交流,共同进步,欢迎大家加入我创建的与前端开发密切相关的技术讨论小组:

努力成为优秀前端工程师!
查看原文

Lucky_LYZ 收藏了问题 · 2020-12-16

有没有前端好用的画流程图的组件?

需求:做一个简单的流程图(可编辑),类似于visio,生成之类的,但是只要求简单的方框流程图即可。线上案例类似于process的流程图生成

Lucky_LYZ 关注了用户 · 2020-07-16

这波能反杀 @meetbo

我的公众号:不知非攻

关注 2003

Lucky_LYZ 收藏了文章 · 2019-09-14

JavaScript的计时器的工作原理

最近都在看一些JavaScript原理层面的文章,恰巧看到了jQuery的作者的一篇关于JavaScript计时器原理的解析,于是诚惶诚恐地决定把原文翻译成中文,一来是为了和大家分享,二来是为了加深自己对于JavaScript的理解。原文链接:http://ejohn.org/blog/how-javascript-timers-work/

原文翻译:

从基础层面来讲,理解JavaScript计时器的工作原理是很重要的。由于JavaScript是单线程的,所以很多时候计时器并不是表现得和我们的直观想象一样。让我们从下面的三个函数开始,它们能够让我们有机会去构造和操作计时器。

  • var id =setTimeout(fn, delay); 创建了一个简单的计时器,在经过给定的时间后,回调函数将会被执行。这个函数会返回一个唯一的ID,便于在之后某个时间可以注销这个计时器。
  • var id = setInterval(fn, delay); -和setTimeout类似,但是每经过一段时间(给定的延时),所传递的函数就会被执行一次,直到这个定时器被注销。
  • clearInterval(id); clearTimeout(id); -接受一个计时器ID(由之前两种计时器返回)并且停止计时器回调函数的执行。

为了理解计时器的内部工作原理,我们首先需要了解一个非常重要的概念:计时器设定的延时是没有保证的。因为所有在浏览器中执行的JavaScript单线程异步事件(比如鼠标点击事件和计时器)都只有在它有空的时候才执行。这最好通过图片来说明,就如下面这张图所示:

图片描述

这一张图片里面有很多信息需要慢慢消化,但是彻底地理解这张图片将会让你对JavaScript异步执行是如何工作的有一个更好的认识。这张图片是从一维的角度来阐述的:在垂直方向是以毫秒计的时间,蓝色的块代表了

当前正在执行的JavaScript代码段。比如第一段JavaScript执行了大概18毫秒,鼠标点击事件大概执行了11毫秒。

由于JavaScript每次只能执行一段代码(基于它单线程的特性),所以所有这些代码段都阻塞了其他异步事件的执行。这就意味着,当一件异步事件(比如鼠标点击,计时器触发和一个XMLHttpRequest 请求完成)触发的时候,这些事件的回调函数将排在执行队列的最后去等待执行(排队的方式因浏览器不同而不同,这里只是一个简化)。

一开始,在第一段代码段内,两个计时器被初始化:一个10ms的setTimeout 和一个10ms的setInterval。由于计时器在哪儿初始化就在那儿开始计时,所以实际上计时器在第一段代码执行完成之前就触发了。然而,计时器的回调函数并不是立即执行了(单线程限制了不能这样做),相反的是,回调函数排在了执行队列的最后,等到下一个有空的时间去执行。

此外,在第一个代码块内我们看到了一个鼠标点击事件发生了。与之相关的javascript异步事件(我们不可能预测用户会在什么时候去采取这样的动作,因此这个事件被视为异步的)并不会立即执行。和计时器一样的是,它被放到了队列的最后去等待执行。

在第一个代码快执行完成的时候,浏览器会立即发出这样的询问:谁正在等待执行?这个时候,鼠标点击处理程序和计时器回调函数都在等待执行。浏览器选择了其中一个(鼠标点击回调函数)并且立即执行它。为了执行,计时器会等到下一个可能执行的时间。

我们注意到,当鼠标点击事件对应的处理程序正在执行的时候,第一个定时回调函数也要执行了。同定时计时器一样,它也在队列的后面等待执行。然而,我们可以注意到,当定时器再一次触发(在计时器回调函数正在执行的时候),这一次定时器回调函数被丢弃了。如果在执行一大块代码块的时候,你把所有的定时回调函数都放在队列的最后,结果就是一大串定时回调函数将会没有间隔的一起执行,直到完成。相反,在把更多定时回调函数放到队列之前,浏览器会静静的等待,知道队列中的所有定时回调函数都执行完成。

事实上,我们可以看到,当interval回调函数正在执行的时候,interval第三次被触发。这给我们一个很重要的信息:interval并不关心当前谁在执行,它的回调函数会不加区分地进入队列,即使存在这个回调函数会被丢弃的可能。

最后,当第二个定时回调函数完成执行的时候,我们可以看到javascript引擎已经没有什么需要执行了。这意味着,浏览器现在正在等待一个新的异步事件的发生。我们可以看到在50ms的时候,定时回调函数再一次被触发。然而,这一次,没有其他代码阻塞他的执行了,所以他立即执行了定时回调函数。

让我们看一个例子来更好地阐述setTimeout 和setInterval的区别。

1 setTimeout(function(){
2     /* Some long block of code... */
3     setTimeout(arguments.callee, 10);
4 }, 10);
5  
6 setInterval(function(){
7     /* Some long block of code... */
8 }, 10);

第一眼看上去这两段代码在功能上是等价的,但事实上却不是。值得注意的是,setTimeout 这段代码会在每次回调函数执行之后至少需要延时10ms再去执行一次(可能是更多,但是不会少)。但是setInterval会每隔10ms就去尝试执行一次回调函数,不管上一个回调函数是不是还在执行。

从这里我们能够学到很多,让我们来概括一下:

  • javascript引擎只有一个线程,迫使异步事件只能加入队列去等待执行。
  • 在执行异步代码的时候,setTimeout 和setInterval 是有着本质区别的。
  • 如果计时器被正在执行的代码阻塞了,它将会进入队列的尾部去等待执行直到下一次可能执行的时间出现(可能超过设定的延时时间)。
  • 如果interval回调函数执行需要花很长时间的话(比指定的延时长),interval有可能没有延迟背靠背地执行。
  • 上述这一切对于理解js引擎是如果工作的无疑是很重要的知识,尤其是大量的典型的异步事件发生时,对于构建一个高效的应用代码片段来说是一个非常有利的基础。

个人见解:

翻译完成之后,感觉对于javascript异步有了新的认识,但是可能初学者看不太懂这篇文章,于是写了一个demo,运行在nodejs环境下(浏览器不容易模拟)

 1 var startTime = new Date();
 2 
 3 //初始化计时器
 4 var start = setTimeout(function() {
 5     var end = new Date();
 6     console.log('10ms的计时器执行完成,距离程序开始' + (end - start) + 'ms');
 7 }, 10);
 8 
 9 //模拟鼠标点击事件
10 function asyncReal(data, callback) {
11     process.nextTick(function() {
12         callback();      
13      });
14 }
15 var asyncStart = new Date();
16 asyncReal('yuanzm', function() {
17     var asyncEnd = new Date();
18     console.log('模拟鼠标执行事件完成,花费时间' + (asyncEnd - asyncStart) + 'ms');
19 })
20 
21 //设定定时器
22 count = 1;
23 var interval = setInterval(function() {
24     ++count;
25     if(count === 5) {
26         clearInterval(interval);
27     }
28     console.log('定时器事件');
29 },10);
30 
31 //模拟第一阶段代码执行
32 var first = [];
33 var start = new Date();
34 for(var i = 0;i < 10000000;i++){
35     first.push(i);
36 }
37 var end = new Date();
38 console.log('第一阶段代码执行完成,用时' + (end - start) + 'ms');

运行结果如下:

图片描述

我们按照文中的原理来解释一下:

  1. 一开始设定的计时器并不是在10ms后立即执行,而是被添加到了队列后面,等到第一阶段代码执行完成才执行,距离开始的时间也不是设定的10ms
  2. 鼠标点击事件同样因为是异步事件,添加到了队列后面,等到第一阶段代码执行完成的时候才执行。
  3. 鼠标点击事件先于计时器事件添加到队列后面
  4. 最后定时器才能执行

郑重声明
本文章属于个人原创,如需转载,请加上原文链接:
http://segmentfault.com/a/1190000002633108
另外同样可以在博客园上面查看本文章:http://www.cnblogs.com/yuanzm/p/4126762.html
也欢迎Follow我的Github:https://github.com/yuanzm

查看原文

Lucky_LYZ 关注了标签 · 2019-09-05

flutter

clipboard.png

Flutter 是 Google 用以帮助开发者在 iOS 和 Android 两个平台开发高质量原生 UI 的移动 SDK。

Flutter is Google’s mobile app SDK for crafting high-quality native interfaces on iOS and Android in record time. Flutter works with existing code, is used by developers and organizations around the world, and is free and open source.

Flutter 官网:https://flutter.dev/
Flutter 中文资源:https://flutter-io.cn/
Flutter Github:https://github.com/flutter/fl...

关注 1017

Lucky_LYZ 收藏了文章 · 2019-08-12

解决npm ERR! Unexpected end of JSON input while parsing near的方法汇总

这两天执行 npm install 时会报错误:

npm ERR! Unexpected end of JSON input while parsing near

这个错误的解决方法有以下几种:

1.删掉package.lock.json

2.清除cache

npm cache clean --force

3.进入下面这个文件夹清除cache
路径:C:/Users/PC/AppData/Roaming/npm-cache
执行:

npm cache clean --force

4.不要用淘宝镜像。

npm set registry https://registry.npmjs.org/

其实我也没搞懂到底是什么问题造成的,有大神给解释一下?

参考资料:https://github.com/vuejs-temp...

查看原文

Lucky_LYZ 收藏了文章 · 2019-08-04

进(线)程,微(宏)任务与http通讯(串联理解)

为了学习Promise,困在了浏览器http流程,进程,线程,宏任务,微任务,页面渲染的漩涡当中,不可自拔,
所以查询许多资料,并根据理解,梳理了他们的关系,希望帮忙指正

流程梳理

进程与线程 http.jpg

1.浏览器进程

进程:浏览器一个页面就是新的一个进程,进程是cpu资源分配的最小单位(系统会给它分配内存);

  1. Browser进程 (http通讯)
  2. 第三方插件进程
  3. GPU进程(加速,3D渲染,一次)
  4. Renderer进程(新开页面渲染进程)

1.1Browser进程(HTTP请求过程)

1.1HTTP请求过程

  1. 域名解析
  2. 发起TCP的三次握手

常见的web server产品有 apache、nginx、IIS、Lighttpd 等

  • 拿到域名对应的IP地址之后,浏览器会以一个随机端口(1024 < 端口 < 65535)向服务器的Web server 80端口发起TCP的连接请求。
  • CP/IP协议栈,还有可能要经过防火墙的过滤,最终到达WEB程序,最终建立了TCP/IP的连接。

 三次握手:

  • 客户端–发送带有SYN标志的数据包–一次握手–服务端
  • 服务端–发送带有SYN/ACK标志的数据包–二次握手–客户端
  • 客户端–发送带有带有ACK标志的数据包–三次握手–服务端
  1. 建立TCP连接后发起http请求
  2. 服务器端响应http请求,浏览器得到html代码
  3. 浏览器处理HTML代码 渲染的主流程
  4. 浏览器对页面进行渲染呈现给用户

HTTP与HTTPS的区别:

  1. HTTP 的URL 以[http://]() 开头,而HTTPS 的URL 以[https://]() 开头
  2. HTTP 是不安全的,而 HTTPS 是安全的
  3. HTTP 标准端口是80 ,而 HTTPS 的标准端口是443
  4. 在OSI 网络模型中,HTTP工作于应用层,而HTTPS 的安全传输机制工作在传输层
  5. HTTP 无法加密,而HTTPS 对传输的数据进行加密
  6. HTTP无需证书,而HTTPS 需要CA机构wosign的颁发的SSL证书

1.4Renderer进程(浏览器渲染进程)

线程:线程包含在每个进程内,线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程);

  1. GUI 渲染线程
  2. JavaScript引擎线程
  3. 定时触发器线程(宏任务(异步任务))
  4. 事件触发线程(宏任务(异步任务))
  5. 异步http请求线程(宏任务(异步任务))

1.4.1GUI 渲染线程

  1. 解析HTML生成DOM树 - 渲染引擎首先解析HTML文档,生成DOM树
  2. 构建Render树 - 接下来不管是内联式,外联式还是嵌入式引入的CSS样式会被解析生成CSSOM树,根据DOM树与CSSOM树生成另外一棵用于渲染的树-渲染树(Render tree),
  3. 布局Render树 - 然后对渲染树的每个节点进行布局处理,确定其在屏幕上的显示位置
  4. 绘制Render树 - 最后遍历渲染树并用UI后端层将每一个节点绘制出来

GUI渲染线程与JS引擎线程互斥

由于JavaScript是可操纵DOM的,如果在修改这些元素属性同时渲染界面(即JS线程和UI线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。
因此为了防止渲染出现不可预期的结果,浏览器设置GUI渲染线程与JS引擎为互斥的关系,当JS引擎执行时GUI线程会被挂起,
GUI更新则会被保存在一个队列中等到JS引擎线程空闲时立即被执行。

1.4.2 JavaScript引擎线程(主线程执行栈)

永远只有JS引擎线程在执行JS脚本程序,
也称为JS内核,负责解析执行Javascript脚本程序的主线程(例如V8引擎)
只能在JavaScript引擎线程(主线程执行栈)执行js脚本

截图未命名.jpg

1.4.2 js引擎执行顺序

  1. 宏任务(同步任务)直接执行,其他线程先进入任务队列等待执行
  2. 然后任务队列中先执行微任务(只有异步任务)
  3. 再执行宏任务(异步任务)(如果有任务内还包含宏任务(同步任务),继续依此执行1)

2016223114314880.jpg

1.4.2宏任务与微任务

宏任务

  • 定时触发器线程(宏任务(异步任务))

    • setTimeout
    • setInterval
    • setImmediate
    • requestAnimationFrame
  • 事件触发线程(宏任务(异步任务))
  • 异步http请求线程(宏任务(异步任务))
  • script方法(宏任务(同步任务))
  • new Promise(宏任务(同步任务)) lijizhixing 

微任务(异步任务)

由于Es6 和node出现产生的微任务

  • Promise.then() catch() finally(),一旦一个pormise有了结果,回调产生一个微任务
  • process.nextTick
  • MutationObserver

参考

查看原文

Lucky_LYZ 收藏了文章 · 2019-08-04

进(线)程,微(宏)任务与http通讯(串联理解)

为了学习Promise,困在了浏览器http流程,进程,线程,宏任务,微任务,页面渲染的漩涡当中,不可自拔,
所以查询许多资料,并根据理解,梳理了他们的关系,希望帮忙指正

流程梳理

进程与线程 http.jpg

1.浏览器进程

进程:浏览器一个页面就是新的一个进程,进程是cpu资源分配的最小单位(系统会给它分配内存);

  1. Browser进程 (http通讯)
  2. 第三方插件进程
  3. GPU进程(加速,3D渲染,一次)
  4. Renderer进程(新开页面渲染进程)

1.1Browser进程(HTTP请求过程)

1.1HTTP请求过程

  1. 域名解析
  2. 发起TCP的三次握手

常见的web server产品有 apache、nginx、IIS、Lighttpd 等

  • 拿到域名对应的IP地址之后,浏览器会以一个随机端口(1024 < 端口 < 65535)向服务器的Web server 80端口发起TCP的连接请求。
  • CP/IP协议栈,还有可能要经过防火墙的过滤,最终到达WEB程序,最终建立了TCP/IP的连接。

 三次握手:

  • 客户端–发送带有SYN标志的数据包–一次握手–服务端
  • 服务端–发送带有SYN/ACK标志的数据包–二次握手–客户端
  • 客户端–发送带有带有ACK标志的数据包–三次握手–服务端
  1. 建立TCP连接后发起http请求
  2. 服务器端响应http请求,浏览器得到html代码
  3. 浏览器处理HTML代码 渲染的主流程
  4. 浏览器对页面进行渲染呈现给用户

HTTP与HTTPS的区别:

  1. HTTP 的URL 以[http://]() 开头,而HTTPS 的URL 以[https://]() 开头
  2. HTTP 是不安全的,而 HTTPS 是安全的
  3. HTTP 标准端口是80 ,而 HTTPS 的标准端口是443
  4. 在OSI 网络模型中,HTTP工作于应用层,而HTTPS 的安全传输机制工作在传输层
  5. HTTP 无法加密,而HTTPS 对传输的数据进行加密
  6. HTTP无需证书,而HTTPS 需要CA机构wosign的颁发的SSL证书

1.4Renderer进程(浏览器渲染进程)

线程:线程包含在每个进程内,线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程);

  1. GUI 渲染线程
  2. JavaScript引擎线程
  3. 定时触发器线程(宏任务(异步任务))
  4. 事件触发线程(宏任务(异步任务))
  5. 异步http请求线程(宏任务(异步任务))

1.4.1GUI 渲染线程

  1. 解析HTML生成DOM树 - 渲染引擎首先解析HTML文档,生成DOM树
  2. 构建Render树 - 接下来不管是内联式,外联式还是嵌入式引入的CSS样式会被解析生成CSSOM树,根据DOM树与CSSOM树生成另外一棵用于渲染的树-渲染树(Render tree),
  3. 布局Render树 - 然后对渲染树的每个节点进行布局处理,确定其在屏幕上的显示位置
  4. 绘制Render树 - 最后遍历渲染树并用UI后端层将每一个节点绘制出来

GUI渲染线程与JS引擎线程互斥

由于JavaScript是可操纵DOM的,如果在修改这些元素属性同时渲染界面(即JS线程和UI线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。
因此为了防止渲染出现不可预期的结果,浏览器设置GUI渲染线程与JS引擎为互斥的关系,当JS引擎执行时GUI线程会被挂起,
GUI更新则会被保存在一个队列中等到JS引擎线程空闲时立即被执行。

1.4.2 JavaScript引擎线程(主线程执行栈)

永远只有JS引擎线程在执行JS脚本程序,
也称为JS内核,负责解析执行Javascript脚本程序的主线程(例如V8引擎)
只能在JavaScript引擎线程(主线程执行栈)执行js脚本

截图未命名.jpg

1.4.2 js引擎执行顺序

  1. 宏任务(同步任务)直接执行,其他线程先进入任务队列等待执行
  2. 然后任务队列中先执行微任务(只有异步任务)
  3. 再执行宏任务(异步任务)(如果有任务内还包含宏任务(同步任务),继续依此执行1)

2016223114314880.jpg

1.4.2宏任务与微任务

宏任务

  • 定时触发器线程(宏任务(异步任务))

    • setTimeout
    • setInterval
    • setImmediate
    • requestAnimationFrame
  • 事件触发线程(宏任务(异步任务))
  • 异步http请求线程(宏任务(异步任务))
  • script方法(宏任务(同步任务))
  • new Promise(宏任务(同步任务)) lijizhixing 

微任务(异步任务)

由于Es6 和node出现产生的微任务

  • Promise.then() catch() finally(),一旦一个pormise有了结果,回调产生一个微任务
  • process.nextTick
  • MutationObserver

参考

查看原文

Lucky_LYZ 收藏了文章 · 2019-08-04

【前端进阶之路】内存基本知识

内存管理

本文以V8为背景

对之前的文章进行重新编辑,内容做了很多的调整,使其具有逻辑更加紧凑,内容更加全面。

1. 基础概念

1.1 生命周期

不管什么程序语言,内存生命周期基本是一致的:

  1. 分配你所需要的内存

  2. 使用分配到的内存(读、写)

  3. 不需要时将其释放、归还

在所有语言中第一和第二部分都很清晰。最后一步在低级语言(例如C语言)中很清晰,但是在像JavaScript等高级语言中,这一步依赖于垃圾回收机制,一般情况下不用程序员操心。

1.2 堆与栈

我们知道,内存空间可以分为栈空间和堆空间,其中

  1. 栈空间:由操作系统自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。

  2. 堆空间:一般由程序员分配释放,这部分空间就要考虑垃圾回收的问题。

1.3 基本类型与引用类型

在JavaScript中

  1. 基本类型:undefined,null,boolean,number,string,在内存中占有固定的大小,他们的值保存在栈空间中,我们通过按值来访问。

  2. 引用类型:Object,Array,Function,则在堆内存中为这个值分配空间,然后把它的内存地址保存在栈内存中。(区分变量和对象)

图片描述

1.4 V8的变量存放

图片描述

  • handle

    handle是指向对象的指针,在V8中,所有对象都是通过handle来引用,handle主要用于V8的垃圾回收机制。进一步的,handle分为两种:

    • 持久化(Persistent handle),存放在堆上

    • 本地化(Local handle),存放在栈上

  • scope

    scope是handle的集合,可以包含若干个handle,这样就无需将每个handle逐次释放,而是直接释放整个scope。

  • context

    context是一个执行器环境,使用context可以将相互分离的JavaScript脚本在同一个V8实例中运行,而不互相干涉。在运行JavaScript脚本时,需要显示的指定context对象。

2. 垃圾回收

2.1 分代策略

脚本中,绝大多数对象的生存期很短,只有某些对象的生存期较长。为利用这一特点,V8将堆进行了分代。对象起初会被分配在新生区。在新生区的内存分配非常容易:我们只需保有一个指向内存区的指针,不断根据新对象的大小对其进行递增即可。当该指针达到了新生区的末尾,就会有一次清理(小周期),清理掉新生区中不活跃的死对象。对于活跃超过2个小周期的对象,则需将其移动至老生区。而在老生区则使用标记清除的算法来进行垃圾回收。V8通过分别对新生代对象和老生代对象使用不同的垃圾回收算法来提升来及回收的效率。这就是所谓的分代策略

默认情况下,64位环境下的V8引擎的新生代内存大小32MB、老生代内存大小为1400MB,而32位则减半,分别为16MB和700MB

根据分代策略,V8将堆空间进行了分隔:

  • 新生区

大多数对象被分配在这里,新生区是一个很小的区域,垃圾回收在这个区域非常频繁,与其他区域相独立。

  • 老生指针区

这里包含大多数可能存储指向其他对象的指针的对象,大多数在新生区存活了一段时间(2个周期)的对象都会被挪到这里。

  • 老生数据区

这里存放只包含原始数据的对象,这些对象没有执行其他对象的指针,例如字符串,数字数组等,它们在新生区存活了一段时间后会被移动到这里。

  • 大对象区

每一个区域都是由一组内存页构成的。除大对象区的内存页较大之外,每个区的内存页都是1MB大小,且按1MB内存对齐。对象超过一定大小时就会被放置到这个区,垃圾回收期从不移动这个区域的对象。

  • 代码区

代码对象,也就是包含JIT之后指令的对象,会被分配到这里。这里是唯一拥有执行权限的内存区。(如果代码对象因过大而被放到大对象区,则该大对象所对应的内存也是可执行的。)

  • Cell区、属性Cell区、Map区

这些区域存放Cell、属性Cell和Map,每个区域因为都是存储相同大小的元素,因此内存结构很简单,这里也是为了方便进行回收。

在 node-v4.x 之后,区域进行了合并为:新生区,老生区,大对象区,Map区,Code区

此外,对于一个对象所占的内存空间,也涉及两个概念:shallow sizeretained size

  • shallow size就是对象本身占用内存的大小,不包含其引用的对象。常规对象(非数组)的shallow size有其成员变量的数量和类型决定

  • retained size是该对象自己的shallow size,加上从该对象能直接或间接访问到对象的shallow size之和。换句话说,retained size是该对象被GC之后所能回收到内存的总和。

这两个概念在使用chrome的开发工具中会看到。

垃圾回收释放的内存即为Retained Size的大小。

2.2 新生区的半空间分配策略

新生代使用半空间(Semi-space)分配策略,其中新对象最初分配在新生代的活跃半空间内。一旦半空间已满,一个Scavenge操作将活跃对象移出到其他半空间中,被认为是长期驻存的对象,并被晋升为老生代。一旦活跃对象已被移出,则在旧的半空间中剩下的任何死亡对象被丢弃。

具体的如下:

YG被平分为两部分空间From和To,所有内存从To空间被分配出去,当To满时,开始触发GC。

例如说:

某时刻,To已经为A、B和C分配了内存,当前它只剩下一小块内存未分配。而From所有的内存都空闲着。

图片描述

此时,一个程序需要为D分配内存,但D需要的内存大小超出了To未分配的内存,此时触发GC,页面停止执行

图片描述

接着From和To进行对换,即原来的To空间被标志为From,From被标志为To。并且把活的变量值(B)标志出来,而垃圾(A、C)未被标志,它们将会被清掉。

图片描述

活跃的变量(B)会被复制到To空间,而垃圾(A、C)则被回收。同时,D被分配到To空间,最后的情况如下。

图片描述

至此,整个GC完成,此过程中页面会阻塞,所以要尽可能的快。

2.2.1 对象的晋升

当一个新生代的对象在满足一定条件下,会从新生代被移到老生代,这就是对象的晋升。具体的移动的标准有两种

  1. 对象从From空间复制到To空间时,会检查它的内存地址来判断这个对象是否经历过一次新生代的清理结果,如果是(说明存活了两个周期了),则赋值到老生代中,否则则赋值到To空间中。

  2. 对象从From空间复制到To空间时,如果To空间已经被使用了超过25%,那么这个对象直接被复制到老生代。

2.3 老生代

V8在老生代中采用Mark-Sweep和Mark-Compact相结合的垃圾回收策略。

2.3.1 标记

标记-清除算法分为标记和清除两个阶段。

标记阶段,所有堆上的活跃对象都会被标记,每个内存页有一个用来标记对象的位图,位图中的每一位对应的内存页中的一个字,这个位图需要占据一定的空间。另外还有两位用来标记对象的状态:

  • 如果一个对象为白对象,表示还未被垃圾回收器发现

  • 如果一个对象为灰对象,表示已经被垃圾回收器发现,但其邻接对象尚未处理

  • 如果一个对象为黑对象,表示已经被垃圾回收器发现,其邻接对象已全部处理

那么这里怎么理解标记的过程?这就必须知道:内存管理方式实际上基于的概念。

GC Root是内存的根节点,在浏览器中它是window,在Nodejs中则是global对象

  • 图的节点名称是创建它的构造函数名

  • 图的边是引用它的属性名或者变量名

有很多内部的GC Root对用户来说都不是很重要,从应用的角度来说有下面几种情况:

  • 全局变量或者全局函数会一直被window这种全局对象所指向,它们会一直占据着内存

  • DOM节点只有在被javascript对象引用的情况下,会留在内存中。

  • 在进行debug或者console的时候,可能会由于保留了上下文,导致本该被释放的对象被保留下来。

实际上,标记的过程正是以由GC Root建立的图为基础,来实现对象的标记,标记算法的核心是深度优先搜索,大致实现如下:

  1. 初始时,位图为空,所有对象都是白对象。

  2. 从根对象(GC Root)到达的对象会被染为灰色,放到一个单独的双端队列中。

  3. 标记阶段,每次都会从双端队列中取出一个对象,并将其转变为黑对象,其邻接对象转变为灰,然后把其邻接对象加入到双端队列中。

  4. 如果双端队列为空或者所有对象都变成黑对象,则结束。

这个算法实现起来还是蛮繁琐的,从的角度来看,其实标记的过程实际上是区分活节点和垃圾节点的过程。

  • 从GC Root开始遍历图,所有能到达的节点称为活节点。

  • GC Root不能到达的节点,该节点就成为垃圾,将会被回收。

标记结束后,所有的对象非黑(活跃节点)即白(垃圾节点)。

标记时间取决于必须标记的活跃对象的数目,对于一个大的web应用,整个堆栈的标记可能需要超过100ms。由于全停顿会造成了浏览器一段时间无响应,所以V8使用了一种增量标记的方式标记活跃对象,将完整的标记拆分成很多小的步骤,每做完一部分就停下来,让JavaScript的应用线程执行一会,这样垃圾回收与应用线程交替执行。V8可以让每个标记步骤的持续时间低于5ms。

举个例子:

window.ob = 2;
window.oa = {
    b1 : 3,
    b2 : {
        c1 : 4,
        c2 : "字符串"
    }
};
window.ob = undefined;

图片描述

例如图中灰色的节点,它原来代表ob变量值,当window.ob = undefined后,此节点与GC Root连接的路径ob被切断了,它就成了垃圾,将会被回收。

2.3.2 清除(Sweep)

由于标记完成后,所有对象都已经被标记,即不是活跃对象就是死亡对象,堆上有多少空间已经确定。清除时,垃圾回收器会扫描连续存放的死对象,将其变成空闲空间。这个任务是由专门的清扫线程同步执行。

2.3.3 整理(Compact)

标记清除有一个问题就是进行一次标记清楚后,内存空间往往是不连续的,会出现很多的内存碎片。如果后续需要分配一个需要内存空间较多的对象时,如果所有的内存碎片都不够用,将会使得V8无法完成这次分配,提前触发垃圾回收。

标记整理正是为了解决标记清除所带来的内存碎片的问题。标记整理在标记清除的基础进行修改,将其的清除阶段变为紧缩极端。在整理的过程中,将活着的对象向内存区的一段移动,移动完成后直接清理掉边界外的内存。紧缩过程涉及对象的移动,所以效率并不是太好,但是能保证不会生成内存碎片。

2.4 垃圾回收总结

  1. 新生代对象的Scavenge,这通常是快速的;

  2. 通过增量方式的标记步骤,依赖于需要标记的对象数量,时间可以任意长;

  3. 完整垃圾回收,这可能需要很长的时间;

  4. 带内存紧缩的完整垃圾回收,这也可能需要很长的时间,需要进行内存紧缩。

3. 内存问题

3.1 内存泄漏

内存泄漏是指计算机可用内存的逐渐减少,原因通常是程序持续无法释放其使用的临时内存。

先来一个最简单的DOM泄漏的例子

var el = document.getElementById("_p");
el.mark = "marked";

//移除P
function removeP() {
    el.parentNode.removeChild(el);
    // el = null;
}

程序非常简单,只是把id为_p的HTML元素从页面移除,在移除之前从GC Root遍历此P元素有两条路可走。在执行removeP()之后,按理来说该元素应该成为垃圾,所占有的内存应该被释放掉,但是由于还存在这路径el没有被切断,p元素占有的内存无法被释放,导致了内存泄漏。

图片描述

3.2 内存占用过多

这个问题很容易理解。例如使用事件代理来减少事件监听的函数,从而减少内存分配的开销。

3.3 gc卡顿

如果你的页面垃圾回收很频繁,那说明你的页面可能内存使用分配太频繁了。频繁的GC可能也会导致页面卡顿。

在一些框架中,如果创建一个大对象之后,可能不会很快就将其释放,而是会缓存起来,直到没有用处为止。

4. chrome dev tools

在使用Chrome进行内存分析的时候,要先在chrome菜单-》工具,或者直接按shift+esc,找到内存管理器,然后选上JavaScript使用的内存(JavaScipt Memory)。

4.1 timeline

通过Timeline的内存模式,可以在宏观上观察到web应用的内存情况,一般我们需要关注的点:

  1. GC的时间长度是否正常?

  2. GC频率是否正常?过于频繁会导致卡顿

  3. 内存趋势图是否正常?

  4. DOM趋势图是否正常?

这些关注点都可以在timeline的内存视图中看到,如图

图片描述

timeline统计的内存变化主要有:

  • js heap:堆空间

  • documents:文档计数

  • node:dom节点数

  • event listener:事件监听器

  • CPU:在手机端暂时没有

此外还可以通过event log看到这期间页面执行的操作

4.2 profile

profile面板我们关注的是Take Heap SnapshotRecode Heap Allocations

图片描述

profile使用必须知道的:

  1. 标志为黄色的表示可能内存泄漏

  2. 标志为红色表示应该是发生内存泄漏

在profile中的几个概念:

  1. (global property):全局对象,还有全局对象引用的对象

  2. (closure):闭包,这里需要关注一下

  3. (compiled code):V8会先代码编译成特定的语言,再执行

  4. (array,string,number,regexp):这些内置对象的引用

  5. HTML..Element:dom对象的引用

4.2.1 Take Heap Snapshot

使用快照,必须知道:

  1. 每次进行快照时,chrome都会先自动执行一个gc

  2. 只有活跃的值,才会反映在快照里

快照有三个视图,它们分别有各自的作用

  1. Summary View

    默认是以概要视图显示的,显示了对象总数,可以展开显示具体内容

  2. Comparison View

    该视图用来对照不同的快照来找到快照之间的差异

  3. Containment View

    在这个视图中,包括三个点

    • DOMWindow objects:js中的全局对象

    • GC Root:VM垃圾回收所使用的GC Root

    • Native Object:被放置到VM中的内置对象

    好吧。暂时不知道有什么用?以后再补充。

4.2.2 Recode Heap Allocations

这个功能可以动态监控,通过次工具可以看到

  1. 什么时候分配了内存,刚刚分配的内存会以深蓝色的柱子表示,柱子越高,内存越大

  2. 什么时候回收了内存,内存被回收的时候,柱子变为灰色

4.3 实践

例子1:timeline来查看正常的内存

图片描述

例子2:通过timeline来发现内存泄漏

图片描述

可以看到随着时间的增长,页面占用的内存越来越多,

在这种情况下就可以怀疑有内存泄漏了,也有可能是浏览器还没有进行gc,这个时候我们可以强制进行垃圾回收(垃圾筒图标)

反复测试,如果发现无论怎么样,内存一直在增长,那么估计你就遇到内存泄漏的问题了。

如果页面中DOM节点的数量一直在攀升,那么肯定出现DOM泄漏了

图片描述

例子3:验证快照之前会进行gc

function Test (s) {
    this.s = s;
}
var _test1 = new Test("__________test___1_________");
var _test2 = new Test("__________test___2_________");
new Test("你看不到我,就是这么神奇");

图片描述

例子4:通过snapshot来发现内存泄漏

  1. 打开例子之后,先进行一次快照

  2. 点击action,代表这用户的交互

  3. 再进行一次快照

  4. 使用comparison视图,对比两次快照,如图

图片描述

可以看到,action之后,内存的数量是增加的(注意,已经gc过了),这说明web应用极有内存泄漏。

一个原则就是找到本不应该存在却还存在的那些值。

例子5:通过内存分配的情况来分析

图片描述

点击蓝色的柱子,可以看到详细的情况,来进行分析

例子6:通过timeline来分析gc过于频繁导致卡顿的问题

图片描述

此例子在移动手机的浏览器进行测试,页面还是相对简单,在比较复杂的移动web应用,这种情况还是比较危险的,可能会导致页面卡死。

参考

  1. MDN:内存管理

  2. Chrome开发者工具之JavaScript内存分析

  3. Google V8的垃圾回收引擎

  4. 测试例子

  5. 如何编写避免垃圾开销的实时Javascript代码

  6. 详解js变量、作用域及内存

  7. V8 concept

  8. 浅谈V8引擎中的垃圾回收机制

  9. 使用 Google V8 引擎开发可定制的应用程序

  10. a tour of v8 garbage collection

查看原文

认证与成就

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

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-10-10
个人主页被 246 人浏览