kukuv2

kukuv2 查看完整档案

北京编辑华北电力大学(北京)  |  电子信息工程 编辑前端技术专家  |  阿里巴巴手淘 编辑填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

kukuv2 评论了文章 · 2019-03-19

Vue项目的自动化测试

Vue项目的自动化测试

说到自动化测试,许多开发团队都是听说过、尝试过,但最后都止步于尝试,不能将TDD(测试驱动开发)、BDD(行为驱动开发)的完整流程贯彻到项目中。思考其中的原因:终究还是成本抵不上收益。

很多后端开发人员可能写过很多自动化的单元测试代码,但是对前端测试一头雾水。这是因为相对于后端开发人员的自动化单元测试,前端的自动化测试成本更高。

自动化测试就是通过自动化脚本将一个又一个测试用例串起来,每个测试用例都要模拟环境、模拟输入、然后断言输出。前端自动化最难的地方就是模拟环境、模拟输入和断言输出了!
我们可以试想一下现实中的使用场景:

模拟环境:首先前端代码是跑在不同的终端环境上的,纯粹的使用某台机子的运行环境进行模拟是无法发现真正存在的问题。所以我们的测试用例必须跑在真实的环境下,这里面包括不同的机器:Android、ios、pc、macbook;不同的系统:window10、window8、linux、mac;不同的运行载体:IE、safari、chrome、firefox、Opera、Android webview、UIWebview、WKWebview;不同网络环境:WiFi、4G、3G、offline

模拟输入:前端的输入不好模拟,在PC上有鼠标click,double Click、drag、mouseDown、mouseOver、input等等,在mobile上有swipe、tap、scroll、摇一摇、屏幕翻转等。相对于后端的单元测试,前端的输入种类繁多,每一种模拟起来都十分复杂,而且很多bug隐藏在几种连贯的输入之后才会复现。

断言输出:前端的断言不是简单的判断值是否相等,很多情况是即使值相等、效果完全不一样。
很多展示效果更是不能通过简单的断言来检测,比如区域是否能滑动,输入时键盘是否正确弹起等。

当你跨越千山万水把上面的问题解决了,测试用例写好了,功能代码写好,完美!然后UE跑过来和你说那个这根线往左边移动一像素的时候,你会瞬间崩溃。可能这一像素你很多测试用例都得重写。所以前端自动化测试的成本真不一定抵得上收益。

但是困难不代表解决不了,部分场景不适合不代表所有场景都不适合!

正因为面临这么多的困难,我们的前端社区开发出了很多工具帮我们解决这些问题。本章节主要是结合Vue这个框架介绍前端自动化测试的一些工具和方法。

我们使用vue-cli去新建一个vue的新项目,在这个项目中开启默认的unit tests和e2e tests

bogon:work xiaorenhui$ vue init webpack vueExample

? Project name vue-example
? Project description A Vue.js project
? Author kukuv <kukuv>
? Vue build standalone
? Install vue-router? No
? Use ESLint to lint your code? No
? Setup unit tests with Karma + Mocha? Yes
? Setup e2e tests with Nightwatch? Yes

下面列举下这个新项目中涉及到的一些开源项目:

  • karma

    • Karma是一个基于Node.js的JavaScript测试执行过程管理工具(Test Runner)。该工具可用于测试所有主流Web浏览器,也可集成到CI(Continuous integration)工具,也可和其他代码编辑器一起使用。这个测试工具的一个强大特性就是,它可以监控(Watch)文件的变化,然后自行执行,通过console.log显示测试结果。
  • Mocha

    • mocha是一款功能丰富的javascript单元测试框架,它既可以运行在nodejs环境中,也可以运行在浏览器环境中。
  • Nightwatch

    • Nightwatch是一套基于Node.js的测试框架,使用Selenium WebDriver API以将Web应用测试自动化。它提供了简单的语法,支持使用JavaScript和CSS选择器,来编写运行在Selenium服务器上的端到端测试。
  • phantomjs

    • 一个基于webkit内核的无头浏览器,即没有UI界面,即它就是一个浏览器,只是其内的点击、翻页等人为相关操作需要程序设计实现。
  • sinon-chai

    • sinon-chai是 sinon和chai这两个断言库的结合,提供丰富的断言方法

很多人看到这么多新名词一定头晕,心想一个单元测试咋需要懂这么多东西。情况是上面只是单元测试框架的一小部分、还有许多框架没有列出来。正因为前端的自动化测试面临着许多问题,所以我们才有这么多的框架来帮忙解决问题。

unit tests

我们先来分析一下这个项目中的unit tests,这里面用到了 Karma、Mocha、sinon-chai、phantomjs。项目中已经有一个默认的单元测试例子。karma作为测试执行过程管理工具把Mocha、sinon-chai、phantomjs等框架组织起来。Mocha用来描述测试用例、sinon-chai用来断言、然后使用phamtomjs作为运行环境来跑测试用例。

npm install 将依赖的库都安装好,这里面phantomjs的依赖会比较难装,如果你之间没有安装过phantom,因为phantom比较大,而且加上国内的网络环境等原因。如果phantomjs装不上可以尝试使用chrome作为运行环境,这需要安装 "karma-chrome-launcher",需要修改配置文件。

然后 npm run unit 跑一下unit tests,如果提示权限问题就 使用sudo 来提升下权限。跑完后我们看一下目录结构

└── unit
    ├── coverage  代码覆盖率报告,src下面的index.html可以直接用浏览器打开
    │   ├── lcov-report
    │   │   ├── base.css
    │   │   ├── index.html
    │   │   ├── prettify.css
    │   │   ├── prettify.js
    │   │   ├── sort-arrow-sprite.png
    │   │   ├── sorter.js
    │   │   └── src
    │   │       ├── App.vue.html
    │   │       ├── components
    │   │       │   ├── Hello.vue.html
    │   │       │   └── index.html
    │   │       └── index.html
    │   └── lcov.info
    ├── index.js 运行测试用例前先加载的文件,方便统计代码覆盖率
    ├── karma.conf.js karma的配置文件
    └── specs 所有的测试用例都放在这里
        └── Hello.spec.js
// 加载所有的测试用例、 testsContext.keys().forEach(testsContext)这种写法是webpack中的加载目录下所有文件的写法

const testsContext = require.context('./specs', true, /\.spec$/)
testsContext.keys().forEach(testsContext)

// 加载所有代码文件,方便统计代码覆盖率
const srcContext = require.context('../../src', true, /^\.\/(?!main(\.js)?$)/)
srcContext.keys().forEach(srcContext)
config.set({
    // 在几个环境里跑你的测试用例
    // browsers: ['PhantomJS','Chrome'], 
    browsers: ['Chrome'],
    // 默认加载几个框架
    frameworks: ['mocha', 'sinon-chai', 'phantomjs-shim'],
    // 使用那些汇报框架
    reporters: ['spec', 'coverage'],
    // 预加载文件
    files: ['./index.js'],
    // 预处理
    preprocessors: {
      './index.js': ['webpack', 'sourcemap']
    },
    // webpack 配置
    webpack: webpackConfig,
    webpackMiddleware: {
      noInfo: true
    },
    // coverage 配置
    coverageReporter: {
      dir: './coverage',
      reporters: [
        { type: 'lcov', subdir: '.' },
        { type: 'text-summary' }
      ]
    }
  })

上面使用的插件例如 mocha、spec、coverage除了karma默认自带的都需要你在npm
上安装对应的插件,例如以下

    "karma": "^1.4.1",
    "karma-chrome-launcher": "^2.2.0",
    "karma-coverage": "^1.1.1",
    "karma-mocha": "^1.3.0",
    "karma-phantomjs-launcher": "^1.0.2",
    "karma-phantomjs-shim": "^1.4.0",
    "karma-sinon-chai": "^1.3.1",
    "karma-sourcemap-loader": "^0.3.7",
    "karma-spec-reporter": "0.0.31",
    "karma-webpack": "^2.0.2",
> vue-exampl@1.0.0 unit /Users/xiaorenhui/work/vueExample
> cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run

07 09 2017 12:08:13.004:INFO [karma]: Karma v1.7.0 server started at http://0.0.0.0:9876/
07 09 2017 12:08:13.007:INFO [launcher]: Launching browser Chrome with unlimited concurrency
07 09 2017 12:08:13.015:INFO [launcher]: Starting browser Chrome
07 09 2017 12:08:15.475:INFO [Chrome 60.0.3112 (Mac OS X 10.12.3)]: Connected on socket qDaxr51TuQCfQBcVAAAA with id 73077049
INFO LOG: 'Download the Vue Devtools extension for a better development experience:
https://github.com/vuejs/vue-devtools'
LOG LOG: 'data'

  Hello.vue
    ✓ should render correct contents

Chrome 60.0.3112 (Mac OS X 10.12.3): Executed 1 of 1 SUCCESS (0.024 secs / 0.011 secs)
TOTAL: 1 SUCCESS


=============================== Coverage summary ===============================
Statements   : 60% ( 3/5 )
Branches     : 50% ( 1/2 )
Functions    : 0% ( 0/1 )
Lines        : 60% ( 3/5 )
================================================================================

我修改了一下Hello.vue这个组件,可以看到coverage 里精确的显示了测试代码的覆盖率,下面是我做的修改

export default {
  name: 'hello',
  data () {
      console.log('data');
      function aa() {

      }
      if(false){
          console.log('data aa');
      }
    return {
      msg: 'Welcome to Your Vue.js App'
    }
  },
  methods:{
      aa(){
          console.log('methods aa');
      }
  }
}


打开reporter下面的index.html我们可以看到代码覆盖的具体情况。
点开Hello.vue更有直观的方式展示哪些代码被覆盖了,哪些没有。
图片描述

e2e测试

既然我们已经有了单元测试,那e2e测试有和单元测试有什么区别呢?Nightwatch是前端e2e测试的一个有代表性的框架。单元测试TDD的粒度很细,我们会为许多函数、方法去写单元测试,而e2e更接近BDD。直白点说,就是TDD的测试单元是一个个函数、方法,而BDD测试的单元是一个个预期的行为表现。e2e做的事情就是打开浏览器,并且真正的访问我们最终的页面,然后在这个真实的浏览器、真实的页面中我们去做各种断言,而单元测试不会要去我们去访问最终的页面,单元测试要保证的是一个个单元是没有问题的,但这些单元组合起来跑在页面上是否有问题,不是单元测试能够保证的,尤其是在前端这种模拟环境、模拟输入非常复杂的领域中,这是单元测试的短板,而e2e测试就是用来解决这些短板的。

我们来看看项目中使用Nightwatch来进行e2e测试的例子

首先看一下目录

├── e2e
│   ├── custom-assertions
│   │   └── elementCount.js 自定义的断言方法
│   ├── nightwatch.conf.js nightwatch的配置文件
│   ├── reports 
│   │   ├── CHROME_60.0.3112.101_Mac\ OS\ X_test.xml
│   │   └── CHROME_60.0.3112.113_Mac\ OS\ X_test.xml
│   ├── runner.js  bootstrap文件,起我们的页面server和nightwatch文件
│   └── specs
│       └── test.js 测试用例

图片描述
selenium是一个用java写的e2e测试工具集,它的API被纳入 w3c的webDriver Api中, nightWatch是对selenium的一个nodejs封装。所有我们需要再配置文件中配置selenium。

  src_folders: ['test/e2e/specs'],
  output_folder: 'test/e2e/reports',
  custom_assertions_path: ['test/e2e/custom-assertions'],
    // 对selenium的配置
  selenium: {
    start_process: true,
    server_path: require('selenium-server').path,
    host: '127.0.0.1',
    port: 4444,
    cli_args: {
      'webdriver.chrome.driver': require('chromedriver').path
    }
  },
    // 测试环境的配置
  test_settings: {
    default: {
      selenium_port: 4444,
      selenium_host: 'localhost',
      silent: true,
      globals: {
        devServerURL: 'http://localhost:' + (process.env.PORT || config.dev.port)
      }
    },

    chrome: {
      desiredCapabilities: {
        browserName: 'chrome',
        javascriptEnabled: true,
        acceptSslCerts: true
      }
    },

    firefox: {
      desiredCapabilities: {
        browserName: 'firefox',
        javascriptEnabled: true,
        acceptSslCerts: true
      }
    }
  }

下面的runner需要先起一个我们的网页服务然后再起nightWatch服务

var server = require('../../build/dev-server.js')

server.ready.then(() => {
  // 2. run the nightwatch test suite against it
  // to run in additional browsers:
  //    1. add an entry in test/e2e/nightwatch.conf.json under "test_settings"
  //    2. add it to the --env flag below
  // or override the environment flag, for example: `npm run e2e -- --env chrome,firefox`
  // For more information on Nightwatch's config file, see
  // http://nightwatchjs.org/guide#settings-file
  var opts = process.argv.slice(2)
    console.log(opts);
  if (opts.indexOf('--config') === -1) {
    opts = opts.concat(['--config', 'test/e2e/nightwatch.conf.js'])
  }
  if (opts.indexOf('--env') === -1) {
    opts = opts.concat(['--env', 'chrome,firefox'])
  }

  var spawn = require('cross-spawn')
  var runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' })

  runner.on('exit', function (code) {
    server.close()
    process.exit(code)
  })

  runner.on('error', function (err) {
    server.close()
    throw err
  })
})

sudo npm run e2e后

> node test/e2e/runner.js

> Starting dev server...

Starting to optimize CSS...
> Listening at http://localhost:8080

[]
Starting selenium server... started - PID:  74459

[Test] Test Suite
=====================

Running:  default e2e tests
 ✔ Element <#app> was visible after 81 milliseconds.
 ✔ Testing if element <.hello> is present.
 ✔ Testing if element <h1> contains text: "Welcome to Your Vue.js App".
 ✔ Testing if element <img> has count: 1

OK. 4 assertions passed. (3.951s)

在控制台上我们能看到各种断言的结果

查看原文

kukuv2 收藏了文章 · 2019-02-21

淘宝新势力周H5性能优化实战

前言

淘宝新势力周(春上新)是命运石kimi链路(H5链路)第一次承接S级大促,面对S级大促内容丰富且复杂的页面需求,kimi链路遇到了一些性能问题,在未进行性能优化之前,搭建出来的页面,业务方普遍反馈页面卡顿严重,无法滑动。

因为时髦女装会场是反馈比较严重的页面之一,所以我以时髦女装会场为例子,介绍下这次性能优化的具体方案。时髦女装会场页面模块在18个左右,页面上的img标签数量在200左右,页面总长度 31208px,以iPhone6页面一屏736px为标准,总共能分为42.4屏左右。为什么我要特别把img标签写出来呢?因为这次的性能卡顿主要的原因是因为错误使用图片懒加载引起的。

通过performance图排查性能问题

现代的web性能优化离不开chrome devtool里performance的帮助,我们先来看一张未优化之前 performance的截图

未优化前

这张performance图我们主要看三个部分:第一个是最上面FPS红线的部分,红线代表着这段时间内未达到60FPS每帧;第二部分是Frames的耗时,勾选了Screenshots后我们能看到每帧的耗时;第三部分是下面函数耗时,我们能从函数耗时里分析出来到底是哪段代码block住了页面渲染,导致卡帧。

从上面的图可以看到最长的一帧耗时3.37秒,这导致FPS都快接近0了。

把函数耗时图拉大分析里面耗时最长的函数,可以看到耗时最长的函数是inview函数,这个函数是图片懒加载库里面检查当前图片是否在屏幕中间的函数。

图片懒加载库的基本逻辑是:当调用初始化函数时立即检查当前页面上所有未真正加载的图片,判断是否需要加载。当页面进行滑动时,重复检查所有图片的逻辑。

这次性能问题的原因和解决方案

卡顿掉帧的原因:这次搭建出来的页面使用的是外包同学开发的业务模块,在模块内部手动调用了lazyLoad初始函数,所以每初始化一个模块就会立即检查所有未加载图片,当页面上图片数量不断增长的时候,inview函数的耗时也不断增加,检查一个图片是否在页面的耗时是2ms~5ms,如果页面中有100个图片未加载当页面滑动时每一次检查会耗时200ms~500ms,如果检查是同步操作的话,掉帧几乎无法避免。

优化方案:之前的其他链路的优化方案是模块懒加载,然后lazyload统一调用,但是因为这次离上线时间较紧张,让外包返工改模块风险较大,于是有另外的一个优化方案:图片懒加载库的异步化,只要避免函数执行耗时过长阻塞渲染,就能避免卡帧,假设我们有100张图片,我们分多批次进行检查,避免一次检查所有图片阻塞渲染。另外针对模块初始化时频繁的检查所有图片的问题,我们给这段逻辑加上debounce函数和图片缓存队列。

优化的过程

优化1.0:

在我接手之前,有一版优化是将模块的渲染通过setTimeout函数改成异步的;这个优化是几乎没有效果的,优化后页面依然卡顿掉帧,因为这个优化并没有找到页面卡顿的原因。起码也应该将setTimeout改成RAF。当然模块的延时加载并不能解决卡顿问题,但是模块的懒加载能解决一部分问题。下面我们看一张使用模块懒加载后的performance图

模块懒加载后,一长条红色块已经变成了短条的红色块,但是因为模块内部单独使用图片懒加载导致频繁检查所有图片是否在可视范围内的问题还是没有得到解决,最长的一帧达到855ms,依然存在掉帧。

优化2.0:

图片懒加载异步版本:通过对图片懒加载库的改造,1、初始化时加上debound优化和图片缓存队列,2、分批检查图片。我们在看一下优化后的performance图

红色的条块也消失,看下面函数执行变的又长有尖,这是因为检查图片的操作变成异步分批了。

图片懒加载库改造时遇到的问题:

在将图片懒加载改造成异步的时候遇到了一个问题,就和Java多线程一样,很多时候异步我们也希望是有序的异步。

分批检查的有序是比较容易保证的,将图片分成多批,一批一批进行,再最后一批结束任务。但是问题出在分批检查和图片懒加载模块初始化存在交替运行的情况,而这两个任务都会改变一个变量。如果不能解决这个问题,就会出现图片有时候能正常加载,有时候加载不出来的情况。所以有说法是,大部分偶现的问题都是异步并行的问题。

解决这个问题的思路也比较常见,就是通过锁,当一个操作异步变量的任务开启,我们的锁自增1,完成异步任务时自减,图片懒加载库的图片缓存初始队列等到异步锁释放后再进行检查,否则存入缓存队列,等待下一帧再检查。

总结

优化过后,对应常见的机型基本能保证页面流畅不卡顿。chrome的performance图基本上和真机操作的情况保持一致,如果performance出现掉帧,那iPhone6s上和android上基本也会出现掉帧,但是iPhone7以上的机器却可能感受不明显。通过performance能够快速定位掉帧的问题,通过解决这些问题实质性的优化页面性能,而不是通过猜测进行无效优化。

查看原文

kukuv2 收藏了文章 · 2019-02-21

JavaScript函数声明与函数表达式

JavaScript函数声明与函数表达式

如何定义一个函数

        在JavaScript里有两种定义函数的方法

  1. 函数声明
            function 函数名称 (参数:可选){ 函数体 }

  2. 函数表达式
            function 函数名称(可选)(参数:可选){ 函数体 }

常见的函数定义以及所属的定义方法

  • function foo(){} 函数声明

  • var bar = function foo(){}; 函数表达式

  • new function bar(){}; 函数表达式

  • function foo(){ function bar(){} 函数声明}

  • (function(){})() 函数表达式

  • +function(){}() 函数表达式

  • !function(){}() 函数表达式

  • ;(function(){})() 函数表达式,分号反正前面没加分号,解析错误

函数声明与函数表达式的一些细微的不同

在JavaScript里函数声明会有一个hoist的过程,也就是说在函数执行的之前,函数体就已经被解析了。一个典型的例子

if (true) {
  function foo() {
    return 'first';
  }
}
else {
  function foo() {
    return 'second';
  }
}
foo();

正常情况下,得到的结果是 second

var foo;
if (true) {
  foo = function() {
    return 'first';
  };
}
else {
  foo = function() {
    return 'second';
  };
}
foo();

我们能得到想要的结果

http://www.nowamagic.net/librarys/veda/detail/1630

查看原文

kukuv2 发布了文章 · 2018-04-13

淘宝新势力周H5性能优化实战

前言

淘宝新势力周(春上新)是命运石kimi链路(H5链路)第一次承接S级大促,面对S级大促内容丰富且复杂的页面需求,kimi链路遇到了一些性能问题,在未进行性能优化之前,搭建出来的页面,业务方普遍反馈页面卡顿严重,无法滑动。

因为时髦女装会场是反馈比较严重的页面之一,所以我以时髦女装会场为例子,介绍下这次性能优化的具体方案。时髦女装会场页面模块在18个左右,页面上的img标签数量在200左右,页面总长度 31208px,以iPhone6页面一屏736px为标准,总共能分为42.4屏左右。为什么我要特别把img标签写出来呢?因为这次的性能卡顿主要的原因是因为错误使用图片懒加载引起的。

通过performance图排查性能问题

现代的web性能优化离不开chrome devtool里performance的帮助,我们先来看一张未优化之前 performance的截图

未优化前

这张performance图我们主要看三个部分:第一个是最上面FPS红线的部分,红线代表着这段时间内未达到60FPS每帧;第二部分是Frames的耗时,勾选了Screenshots后我们能看到每帧的耗时;第三部分是下面函数耗时,我们能从函数耗时里分析出来到底是哪段代码block住了页面渲染,导致卡帧。

从上面的图可以看到最长的一帧耗时3.37秒,这导致FPS都快接近0了。

把函数耗时图拉大分析里面耗时最长的函数,可以看到耗时最长的函数是inview函数,这个函数是图片懒加载库里面检查当前图片是否在屏幕中间的函数。

图片懒加载库的基本逻辑是:当调用初始化函数时立即检查当前页面上所有未真正加载的图片,判断是否需要加载。当页面进行滑动时,重复检查所有图片的逻辑。

这次性能问题的原因和解决方案

卡顿掉帧的原因:这次搭建出来的页面使用的是外包同学开发的业务模块,在模块内部手动调用了lazyLoad初始函数,所以每初始化一个模块就会立即检查所有未加载图片,当页面上图片数量不断增长的时候,inview函数的耗时也不断增加,检查一个图片是否在页面的耗时是2ms~5ms,如果页面中有100个图片未加载当页面滑动时每一次检查会耗时200ms~500ms,如果检查是同步操作的话,掉帧几乎无法避免。

优化方案:之前的其他链路的优化方案是模块懒加载,然后lazyload统一调用,但是因为这次离上线时间较紧张,让外包返工改模块风险较大,于是有另外的一个优化方案:图片懒加载库的异步化,只要避免函数执行耗时过长阻塞渲染,就能避免卡帧,假设我们有100张图片,我们分多批次进行检查,避免一次检查所有图片阻塞渲染。另外针对模块初始化时频繁的检查所有图片的问题,我们给这段逻辑加上debounce函数和图片缓存队列。

优化的过程

优化1.0:

在我接手之前,有一版优化是将模块的渲染通过setTimeout函数改成异步的;这个优化是几乎没有效果的,优化后页面依然卡顿掉帧,因为这个优化并没有找到页面卡顿的原因。起码也应该将setTimeout改成RAF。当然模块的延时加载并不能解决卡顿问题,但是模块的懒加载能解决一部分问题。下面我们看一张使用模块懒加载后的performance图

模块懒加载后,一长条红色块已经变成了短条的红色块,但是因为模块内部单独使用图片懒加载导致频繁检查所有图片是否在可视范围内的问题还是没有得到解决,最长的一帧达到855ms,依然存在掉帧。

优化2.0:

图片懒加载异步版本:通过对图片懒加载库的改造,1、初始化时加上debound优化和图片缓存队列,2、分批检查图片。我们在看一下优化后的performance图

红色的条块也消失,看下面函数执行变的又长有尖,这是因为检查图片的操作变成异步分批了。

图片懒加载库改造时遇到的问题:

在将图片懒加载改造成异步的时候遇到了一个问题,就和Java多线程一样,很多时候异步我们也希望是有序的异步。

分批检查的有序是比较容易保证的,将图片分成多批,一批一批进行,再最后一批结束任务。但是问题出在分批检查和图片懒加载模块初始化存在交替运行的情况,而这两个任务都会改变一个变量。如果不能解决这个问题,就会出现图片有时候能正常加载,有时候加载不出来的情况。所以有说法是,大部分偶现的问题都是异步并行的问题。

解决这个问题的思路也比较常见,就是通过锁,当一个操作异步变量的任务开启,我们的锁自增1,完成异步任务时自减,图片懒加载库的图片缓存初始队列等到异步锁释放后再进行检查,否则存入缓存队列,等待下一帧再检查。

总结

优化过后,对应常见的机型基本能保证页面流畅不卡顿。chrome的performance图基本上和真机操作的情况保持一致,如果performance出现掉帧,那iPhone6s上和android上基本也会出现掉帧,但是iPhone7以上的机器却可能感受不明显。通过performance能够快速定位掉帧的问题,通过解决这些问题实质性的优化页面性能,而不是通过猜测进行无效优化。

查看原文

赞 37 收藏 54 评论 4

kukuv2 赞了回答 · 2018-01-19

解决webstorm升级2017.1后vue文件中的sass报错

试试<style type="text/scss" lang="scss"></style>less是可以的

关注 6 回答 5

kukuv2 发布了文章 · 2017-09-12

Vue的异构

Vue的异构

组件化是至上而下的,一旦一个页面从某个Dom节点开始逐渐话之后,相当于从这个Dom节点开始所有的子辈节点都被组件化框架所接管了,这是因为Vue或React这类组件化框架都有一个重要的特性:那就是 数据=》视图 的一一对应关系,所以在被组件化框架所接管的区域内,所以的Dom操作都应该有组件化框架来完成,如果中间有其他的Dom操作,比如jQuery修改Dom节点元素,会破坏 数据=》视图 这个一一对应关系。

但是事实上,我们有大量的网页系统和框架并不是基于组件化框架所搭建的,所以很多时候我们必须拿出异构的方案。这里的异构指的是 组件化框架和其他框架共存的情况。

我们主要介绍Vue异构方法,实际上Vue提供的computed、watch 、directive等对异构提供了很大的帮助。所以Vue的异构相对其他组件化框架来说更加容易理解和操作。

组件化异构的核心思想就是在不破坏 数据=》视图 的前提下,将非组件化的代码封装成类组件化的代码。

不属于异构的情况

在列举Vue的异构方法之前,我们要分清楚什么情况才称之为异构,并不是引入了任意js库就叫异构。只有引入其他js库,并且我们使用该库去操作Vue所接管的Dom节点时才称之为异构。

一个不是异构的例子

我们从github上下载Vue的源代码,里面有一些实用的例子在example文件夹里,其中有一个《elastic-header》的例子,我们能看到这个例子里引用了dynamic.js这个js库来实现平滑的动画效果。那这里面算不算异构能,需不需要我们采取什么特殊处理呢?会不会破坏 数据=》视图 的映射关系呢?其实是不会的!
Alt text
我们看这里面使用dynamics.js的方法,dynamics被当成一个工具库来使用,并没有直接操作Dom,而是改变Vue里面对象的数值,最终是由Vue去操作Dom,所以并不会破坏 数据=》视图 的映射关系,所以我们并不需要担心引入这类的库与Vue混用会产生问题。

下面我们对Vue的异构方法做一些分类,不同的异构需求关注的重心也不太一样。

通过封装成Vue组件的方式实现异构

在Vue的example库里就有一个Vue异构的例子,在example/select2这个目录下。这个例子是Vue与jQuery的插件select2进行异构的例子 ,并且是通过将select2封装成Vue组件的方式实现异构的。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Vue.js wrapper component example (jquery plugin: select2)</title>
    <!-- Delete ".min" for console warnings in development -->
    <script data-original="../../dist/vue.min.js"></script>
    <script data-original="https://unpkg.com/jquery"></script>
    <script data-original="https://unpkg.com/select2@4.0.3"></script>
    <link href="https://unpkg.com/select2@4.0.3/dist/css/select2.min.css" rel="stylesheet">
    <style>
      html, body {
        font: 13px/18px sans-serif;
      }
      select {
        min-width: 300px;
      }
    </style>
  </head>
  <body>

    <div id="el">
    </div>

    <!-- using string template here to work around HTML <option> placement restriction -->
    <script type="text/x-template" id="demo-template">
      <div>
        <p>Selected: {{ selected }}</p>
        <select2 :options="options" v-model="selected">
          <option disabled value="0">Select one</option>
        </select2>
      </div>
    </script>

    <script type="text/x-template" id="select2-template">
      <select>
        <slot></slot>
      </select>
    </script>

    <script>
    Vue.component('select2', {
      props: ['options', 'value'],
      template: '#select2-template',
      mounted: function () {
        var vm = this
        $(this.$el)
          .val(this.value)
          // init select2
          .select2({ data: this.options })
          // emit event on change.
          .on('change', function () {
            vm.$emit('input', this.value)
          })
      },
      watch: {
        value: function (value) {
          // update value
          $(this.$el).val(value).trigger('change')
        },
        options: function (options) {
          // update options
          $(this.$el).select2({ data: options })
        }
      },
      destroyed: function () {
        $(this.$el).off().select2('destroy')
      }
    })

    var vm = new Vue({
      el: '#el',
      template: '#demo-template',
      data: {
        selected: 1,
        options: [
          { id: 1, text: 'Hello' },
          { id: 2, text: 'World' }
        ]
      }
    })
    </script>
  </body>
</html>

这里面最关键的代码就是 组件的mounted方法和watch方法,异构最关键的事情有三件

  • Vue要初始化其他库(组件化异构的时候一般在mounted方法里,因为这个时候能拿到Dom元素)
  • 当Vue数据发生变化的时候要调用其他库的方法更新页面
  • 当其他库的数据方法发生变化的时候要对Vue中的数据重新赋值,保证 数据=》视图这个对应关系不变
$(this.$el)
          .val(this.value)
          // 在mounted方法里初始化 select2库
          .select2({ data: this.options })
          // 当其他库的数据方法发生变化的时候要对Vue中的数据重新赋值,保证 数据=》视图这个对应关系不变
          .on('change', function () {
            vm.$emit('input', this.value)
          })
watch: {
        // 当Vue数据发生变化的时候要调用其他库的方法更新页面
        value: function (value) {
          // update value
          $(this.$el).val(value).trigger('change')
        },
        options: function (options) {
          // update options
          $(this.$el).select2({ data: options })
        }
      },

通过directive的方式实现异构

下面我们将官方提供的例子修改一下,改成由 directive的方式来实现异构。因为指令在Vue2里能力已经被极大的削弱,但也依然具备能够实现异构的能力

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Vue.js wrapper component example (jquery plugin: select2)</title>
    <!-- Delete ".min" for console warnings in development -->
    <script data-original="../../dist/vue.min.js"></script>
    <script data-original="https://unpkg.com/jquery"></script>
    <script data-original="https://unpkg.com/select2@4.0.3"></script>
    <link href="https://unpkg.com/select2@4.0.3/dist/css/select2.min.css"
          rel="stylesheet">
    <style>
        html, oubody {
            font: 13px/18px sans-serif;
        }

        select {
            min-width: 300px;
        }
    </style>
</head>
<body>

<div id="el"></div>

<!-- using string template here to work around HTML <option> placement restriction -->
<script type="text/x-template"
        id="demo-template">
    <div>
        <p>Selected: {{ selected }}</p>
        // 使用v-select指令
        <select v-select="{options:options,onChange:onChange,value:selected}"></select>
    </div>
</script>

<script>
    Vue.directive('select', {
        // 当绑定元素插入到 DOM 中。
        bind: function (el, value) {
            console.log('bind ' + arguments);
        },
        inserted: function (el, binding) {
            var value = binding.value
            // 聚焦元素
            $(el)
                    .val(value.value)
                    // init select2
                    .select2({data: value.options})
                    // emit event on change.
                    .on('select2:select', function (evt) {
                        var id = evt.params.data.id
                        value.onChange(id)
                    })
            console.log('inserted ' + arguments);
        },
        update: function (el, binding) {
            var val = $(el).val()
            var value = binding.value.value
            if (val !== value) {
                $(el).val(value).trigger('change')
            }
        },
        componentUpdated: function () {
            console.log('componentUpdated ' + arguments);
        },
        unbind: function () {
            console.log('unbind ' + arguments);
        }
    })

    var vm = new Vue({
        el: '#el',
        template: '#demo-template',
        data: {
            selected: 1,
            options: [
                {
                    id: 1,
                    text: 'Hello'
                },
                {
                    id: 2,
                    text: 'World'
                }
            ]
        },
        methods: {
            onChange: function (val) {
                debugger;
                this.selected = val
            }
        }
    })
</script>
</body>
</html>

因为指令无法直接获取到vm,所以我们通过binding里传入onChange方法来更改组件的值。

这里面最关键的代码就是指令的inserted方法和update方法

  • 指令要初始化其他库(在inserted方法里,因为这时候能获取到对应的Dom节点)
  • 当Vue数据发生变化的时候要调用其他库的方法更新页面 (在update方法里调用select2库的更新方法)
  • 当其他库的数据方法发生变化的时候要对Vue中的数据重新赋值,保证 数据=》视图这个对应关系不变(注册事件监听,调用指令的binding.value值里的onChange方法)

循环嵌套Vue组件

想象中很复杂的异构其实只要把数据到视图的逻辑都理清楚,其实实现起来非常容易。

我们在想象一个应用场景,如果这时候需要在select2这个jQuery插件库中再插入 Vue组件应该怎么处理呢?

首先在由其他库中生成的Dom节点中插入Vue组件,只能通过 new Vue()方法去重新初始化Vue组件。但是需要注意一点的是组件是一个可以多实例的概念,所以我们不能简单的给嵌套Vue组件一个Id,因为当多实例的情况下通过这个Id能取到不只一个Dom节点,所以我们最佳方式是通过Dom节点树向下寻找,直到找到具体拿来初始化的对应元素,可以通过 jQuery 的 find方法,也能通过生成 uuid的方式来生成唯一的Id,通过这些方式来保证组件在多实例的情况下运行起来也不会出现问题。

查看原文

赞 3 收藏 4 评论 0

kukuv2 发布了文章 · 2017-09-07

Vue项目的自动化测试

Vue项目的自动化测试

说到自动化测试,许多开发团队都是听说过、尝试过,但最后都止步于尝试,不能将TDD(测试驱动开发)、BDD(行为驱动开发)的完整流程贯彻到项目中。思考其中的原因:终究还是成本抵不上收益。

很多后端开发人员可能写过很多自动化的单元测试代码,但是对前端测试一头雾水。这是因为相对于后端开发人员的自动化单元测试,前端的自动化测试成本更高。

自动化测试就是通过自动化脚本将一个又一个测试用例串起来,每个测试用例都要模拟环境、模拟输入、然后断言输出。前端自动化最难的地方就是模拟环境、模拟输入和断言输出了!
我们可以试想一下现实中的使用场景:

模拟环境:首先前端代码是跑在不同的终端环境上的,纯粹的使用某台机子的运行环境进行模拟是无法发现真正存在的问题。所以我们的测试用例必须跑在真实的环境下,这里面包括不同的机器:Android、ios、pc、macbook;不同的系统:window10、window8、linux、mac;不同的运行载体:IE、safari、chrome、firefox、Opera、Android webview、UIWebview、WKWebview;不同网络环境:WiFi、4G、3G、offline

模拟输入:前端的输入不好模拟,在PC上有鼠标click,double Click、drag、mouseDown、mouseOver、input等等,在mobile上有swipe、tap、scroll、摇一摇、屏幕翻转等。相对于后端的单元测试,前端的输入种类繁多,每一种模拟起来都十分复杂,而且很多bug隐藏在几种连贯的输入之后才会复现。

断言输出:前端的断言不是简单的判断值是否相等,很多情况是即使值相等、效果完全不一样。
很多展示效果更是不能通过简单的断言来检测,比如区域是否能滑动,输入时键盘是否正确弹起等。

当你跨越千山万水把上面的问题解决了,测试用例写好了,功能代码写好,完美!然后UE跑过来和你说那个这根线往左边移动一像素的时候,你会瞬间崩溃。可能这一像素你很多测试用例都得重写。所以前端自动化测试的成本真不一定抵得上收益。

但是困难不代表解决不了,部分场景不适合不代表所有场景都不适合!

正因为面临这么多的困难,我们的前端社区开发出了很多工具帮我们解决这些问题。本章节主要是结合Vue这个框架介绍前端自动化测试的一些工具和方法。

我们使用vue-cli去新建一个vue的新项目,在这个项目中开启默认的unit tests和e2e tests

bogon:work xiaorenhui$ vue init webpack vueExample

? Project name vue-example
? Project description A Vue.js project
? Author kukuv <kukuv>
? Vue build standalone
? Install vue-router? No
? Use ESLint to lint your code? No
? Setup unit tests with Karma + Mocha? Yes
? Setup e2e tests with Nightwatch? Yes

下面列举下这个新项目中涉及到的一些开源项目:

  • karma

    • Karma是一个基于Node.js的JavaScript测试执行过程管理工具(Test Runner)。该工具可用于测试所有主流Web浏览器,也可集成到CI(Continuous integration)工具,也可和其他代码编辑器一起使用。这个测试工具的一个强大特性就是,它可以监控(Watch)文件的变化,然后自行执行,通过console.log显示测试结果。
  • Mocha

    • mocha是一款功能丰富的javascript单元测试框架,它既可以运行在nodejs环境中,也可以运行在浏览器环境中。
  • Nightwatch

    • Nightwatch是一套基于Node.js的测试框架,使用Selenium WebDriver API以将Web应用测试自动化。它提供了简单的语法,支持使用JavaScript和CSS选择器,来编写运行在Selenium服务器上的端到端测试。
  • phantomjs

    • 一个基于webkit内核的无头浏览器,即没有UI界面,即它就是一个浏览器,只是其内的点击、翻页等人为相关操作需要程序设计实现。
  • sinon-chai

    • sinon-chai是 sinon和chai这两个断言库的结合,提供丰富的断言方法

很多人看到这么多新名词一定头晕,心想一个单元测试咋需要懂这么多东西。情况是上面只是单元测试框架的一小部分、还有许多框架没有列出来。正因为前端的自动化测试面临着许多问题,所以我们才有这么多的框架来帮忙解决问题。

unit tests

我们先来分析一下这个项目中的unit tests,这里面用到了 Karma、Mocha、sinon-chai、phantomjs。项目中已经有一个默认的单元测试例子。karma作为测试执行过程管理工具把Mocha、sinon-chai、phantomjs等框架组织起来。Mocha用来描述测试用例、sinon-chai用来断言、然后使用phamtomjs作为运行环境来跑测试用例。

npm install 将依赖的库都安装好,这里面phantomjs的依赖会比较难装,如果你之间没有安装过phantom,因为phantom比较大,而且加上国内的网络环境等原因。如果phantomjs装不上可以尝试使用chrome作为运行环境,这需要安装 "karma-chrome-launcher",需要修改配置文件。

然后 npm run unit 跑一下unit tests,如果提示权限问题就 使用sudo 来提升下权限。跑完后我们看一下目录结构

└── unit
    ├── coverage  代码覆盖率报告,src下面的index.html可以直接用浏览器打开
    │   ├── lcov-report
    │   │   ├── base.css
    │   │   ├── index.html
    │   │   ├── prettify.css
    │   │   ├── prettify.js
    │   │   ├── sort-arrow-sprite.png
    │   │   ├── sorter.js
    │   │   └── src
    │   │       ├── App.vue.html
    │   │       ├── components
    │   │       │   ├── Hello.vue.html
    │   │       │   └── index.html
    │   │       └── index.html
    │   └── lcov.info
    ├── index.js 运行测试用例前先加载的文件,方便统计代码覆盖率
    ├── karma.conf.js karma的配置文件
    └── specs 所有的测试用例都放在这里
        └── Hello.spec.js
// 加载所有的测试用例、 testsContext.keys().forEach(testsContext)这种写法是webpack中的加载目录下所有文件的写法

const testsContext = require.context('./specs', true, /\.spec$/)
testsContext.keys().forEach(testsContext)

// 加载所有代码文件,方便统计代码覆盖率
const srcContext = require.context('../../src', true, /^\.\/(?!main(\.js)?$)/)
srcContext.keys().forEach(srcContext)
config.set({
    // 在几个环境里跑你的测试用例
    // browsers: ['PhantomJS','Chrome'], 
    browsers: ['Chrome'],
    // 默认加载几个框架
    frameworks: ['mocha', 'sinon-chai', 'phantomjs-shim'],
    // 使用那些汇报框架
    reporters: ['spec', 'coverage'],
    // 预加载文件
    files: ['./index.js'],
    // 预处理
    preprocessors: {
      './index.js': ['webpack', 'sourcemap']
    },
    // webpack 配置
    webpack: webpackConfig,
    webpackMiddleware: {
      noInfo: true
    },
    // coverage 配置
    coverageReporter: {
      dir: './coverage',
      reporters: [
        { type: 'lcov', subdir: '.' },
        { type: 'text-summary' }
      ]
    }
  })

上面使用的插件例如 mocha、spec、coverage除了karma默认自带的都需要你在npm
上安装对应的插件,例如以下

    "karma": "^1.4.1",
    "karma-chrome-launcher": "^2.2.0",
    "karma-coverage": "^1.1.1",
    "karma-mocha": "^1.3.0",
    "karma-phantomjs-launcher": "^1.0.2",
    "karma-phantomjs-shim": "^1.4.0",
    "karma-sinon-chai": "^1.3.1",
    "karma-sourcemap-loader": "^0.3.7",
    "karma-spec-reporter": "0.0.31",
    "karma-webpack": "^2.0.2",
> vue-exampl@1.0.0 unit /Users/xiaorenhui/work/vueExample
> cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run

07 09 2017 12:08:13.004:INFO [karma]: Karma v1.7.0 server started at http://0.0.0.0:9876/
07 09 2017 12:08:13.007:INFO [launcher]: Launching browser Chrome with unlimited concurrency
07 09 2017 12:08:13.015:INFO [launcher]: Starting browser Chrome
07 09 2017 12:08:15.475:INFO [Chrome 60.0.3112 (Mac OS X 10.12.3)]: Connected on socket qDaxr51TuQCfQBcVAAAA with id 73077049
INFO LOG: 'Download the Vue Devtools extension for a better development experience:
https://github.com/vuejs/vue-devtools'
LOG LOG: 'data'

  Hello.vue
    ✓ should render correct contents

Chrome 60.0.3112 (Mac OS X 10.12.3): Executed 1 of 1 SUCCESS (0.024 secs / 0.011 secs)
TOTAL: 1 SUCCESS


=============================== Coverage summary ===============================
Statements   : 60% ( 3/5 )
Branches     : 50% ( 1/2 )
Functions    : 0% ( 0/1 )
Lines        : 60% ( 3/5 )
================================================================================

我修改了一下Hello.vue这个组件,可以看到coverage 里精确的显示了测试代码的覆盖率,下面是我做的修改

export default {
  name: 'hello',
  data () {
      console.log('data');
      function aa() {

      }
      if(false){
          console.log('data aa');
      }
    return {
      msg: 'Welcome to Your Vue.js App'
    }
  },
  methods:{
      aa(){
          console.log('methods aa');
      }
  }
}


打开reporter下面的index.html我们可以看到代码覆盖的具体情况。
点开Hello.vue更有直观的方式展示哪些代码被覆盖了,哪些没有。
图片描述

e2e测试

既然我们已经有了单元测试,那e2e测试有和单元测试有什么区别呢?Nightwatch是前端e2e测试的一个有代表性的框架。单元测试TDD的粒度很细,我们会为许多函数、方法去写单元测试,而e2e更接近BDD。直白点说,就是TDD的测试单元是一个个函数、方法,而BDD测试的单元是一个个预期的行为表现。e2e做的事情就是打开浏览器,并且真正的访问我们最终的页面,然后在这个真实的浏览器、真实的页面中我们去做各种断言,而单元测试不会要去我们去访问最终的页面,单元测试要保证的是一个个单元是没有问题的,但这些单元组合起来跑在页面上是否有问题,不是单元测试能够保证的,尤其是在前端这种模拟环境、模拟输入非常复杂的领域中,这是单元测试的短板,而e2e测试就是用来解决这些短板的。

我们来看看项目中使用Nightwatch来进行e2e测试的例子

首先看一下目录

├── e2e
│   ├── custom-assertions
│   │   └── elementCount.js 自定义的断言方法
│   ├── nightwatch.conf.js nightwatch的配置文件
│   ├── reports 
│   │   ├── CHROME_60.0.3112.101_Mac\ OS\ X_test.xml
│   │   └── CHROME_60.0.3112.113_Mac\ OS\ X_test.xml
│   ├── runner.js  bootstrap文件,起我们的页面server和nightwatch文件
│   └── specs
│       └── test.js 测试用例

图片描述
selenium是一个用java写的e2e测试工具集,它的API被纳入 w3c的webDriver Api中, nightWatch是对selenium的一个nodejs封装。所有我们需要再配置文件中配置selenium。

  src_folders: ['test/e2e/specs'],
  output_folder: 'test/e2e/reports',
  custom_assertions_path: ['test/e2e/custom-assertions'],
    // 对selenium的配置
  selenium: {
    start_process: true,
    server_path: require('selenium-server').path,
    host: '127.0.0.1',
    port: 4444,
    cli_args: {
      'webdriver.chrome.driver': require('chromedriver').path
    }
  },
    // 测试环境的配置
  test_settings: {
    default: {
      selenium_port: 4444,
      selenium_host: 'localhost',
      silent: true,
      globals: {
        devServerURL: 'http://localhost:' + (process.env.PORT || config.dev.port)
      }
    },

    chrome: {
      desiredCapabilities: {
        browserName: 'chrome',
        javascriptEnabled: true,
        acceptSslCerts: true
      }
    },

    firefox: {
      desiredCapabilities: {
        browserName: 'firefox',
        javascriptEnabled: true,
        acceptSslCerts: true
      }
    }
  }

下面的runner需要先起一个我们的网页服务然后再起nightWatch服务

var server = require('../../build/dev-server.js')

server.ready.then(() => {
  // 2. run the nightwatch test suite against it
  // to run in additional browsers:
  //    1. add an entry in test/e2e/nightwatch.conf.json under "test_settings"
  //    2. add it to the --env flag below
  // or override the environment flag, for example: `npm run e2e -- --env chrome,firefox`
  // For more information on Nightwatch's config file, see
  // http://nightwatchjs.org/guide#settings-file
  var opts = process.argv.slice(2)
    console.log(opts);
  if (opts.indexOf('--config') === -1) {
    opts = opts.concat(['--config', 'test/e2e/nightwatch.conf.js'])
  }
  if (opts.indexOf('--env') === -1) {
    opts = opts.concat(['--env', 'chrome,firefox'])
  }

  var spawn = require('cross-spawn')
  var runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' })

  runner.on('exit', function (code) {
    server.close()
    process.exit(code)
  })

  runner.on('error', function (err) {
    server.close()
    throw err
  })
})

sudo npm run e2e后

> node test/e2e/runner.js

> Starting dev server...

Starting to optimize CSS...
> Listening at http://localhost:8080

[]
Starting selenium server... started - PID:  74459

[Test] Test Suite
=====================

Running:  default e2e tests
 ✔ Element <#app> was visible after 81 milliseconds.
 ✔ Testing if element <.hello> is present.
 ✔ Testing if element <h1> contains text: "Welcome to Your Vue.js App".
 ✔ Testing if element <img> has count: 1

OK. 4 assertions passed. (3.951s)

在控制台上我们能看到各种断言的结果

查看原文

赞 36 收藏 49 评论 8

kukuv2 发布了文章 · 2017-03-31

javascript典型内存泄漏及chrome的排查方法

javascript的内存泄漏

对于JavaScript这门语言的使用者来说,大多数的使用者的内存管理意识都不强。因为JavaScript一直以来都只作为在网页上使用的脚本语言,而网页往往都不会长时间的运行,所以使用者对JavaScript的运行时长和内存控制都比较漠视。但随着Spa(单页应用)、node.js服务端程序和各种js工具的诞生,我们需要重新重视JavaScript的内存管理。

内存泄漏的定义

指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

JavaScript的内存管理

首先JavaScript是一个有Garbage Collection 的语言,也就是我们不需要手动的回收内存。不同的JavaScript引擎有不同的垃圾回收机制,这里我们主要以V8这个被广泛使用的JavaScript引擎为主。

JavaScript内存分配和回收的关键词:GC根、作用域

GC根:一般指全局且不会被垃圾回收的对象,比如:window、document或者是页面上存在的dom元素。JavaScript的垃圾回收算法会判断某块对象内存是否是GC根可达(存在一条由GC根对象到该对象的引用),如果不是那这块内存将会被标记回收。

作用域:在JavaScript的作用域里,我们能够新建对象来分配内存。比如说调用函数,函数执行的过程中就会创建一块作用域,如果是创建的是作用域内的局部对象,当作用域运行结束后,所有的局部对象(GC根无法触及)都会被标记回收,在JavaScript中能引起作用域分配的有函数调用、with和全局作用域。

作用域的分类:局部作用域、全局作用域、闭包作用域

局部作用域

函数调用会创建局部作用域,在局部作用域中的新建的对象,如果函数运行结束后,该对象没有作用域外部的引用,那该对象将会标记回收

全局作用域

每个JavaScript进程都会有一个全局作用域,全局作用域上的引用的对象都是常驻内存的,直到进程退出内存才会自动释放。
手动释放全局作用域上的引用的对象有两种方式:

  • global.foo = undefined

重新赋值改变引用

  • delete global.foo

删除对象属性

闭包作用域

在JavaScript语言中有闭包的概念,闭包指的是包含自由变量的代码块、自由变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)。

var closure = (function(){
    //这里是闭包的作用域
    var i = 0 // i就是自由变量
    return function(){
        console.log(i++)
    }
})()

闭包作用域会保持对自由变量的引用。上面代码的引用链就是:

window -> closure -> i

闭包作用域还有一个重要的概念,闭包对象是当前作用域中的所有内部函数作用域共享的,并且这个当前作用域的闭包对象中除了包含一条指向上一层作用域闭包对象的引用外,其余的存储的变量引用一定是当前作用域中的所有内部函数作用域中使用到的变量

常见的几种内存泄漏的方式及使用chrome dev tools的排查方法

用全局变量缓存数据

将全局变量作为缓存数据的一种方式,将之后要用到的数据都挂载到全局变量上,用完之后也不手动释放内存(因为全局变量引用的对象,垃圾回收机制不会自动回收),全局变量逐渐就积累了一些不用的对象,导致内存泄漏

   var x = [];
    function createSomeNodes() {
        var div;
        var i = 10000;
        var frag = document.createDocumentFragment();
        for (; i > 0; i--) {
            div = document.createElement("div");
            div.appendChild(document.createTextNode(i + " - " + new Date().toTimeString()));
            frag.appendChild(div);
        }
        document.getElementById("nodes").appendChild(frag);
    }
    function grow() {
        x.push(new Array(1000000).join('x'));
        createSomeNodes();
        setTimeout(grow, 1000);
    }
    grow()

上面的代码贴一张 timeline的截图
图片描述
主要看memory区域,通过分析代码我们可以知道页面上的dom节点是不断增加的,所以memory里绿色的线(代表dom nodes)也是不断升高的;而代表js heap的蓝色的线是有升有降,当整体趋势是逐渐升高,这是因为js 有内存回收机制,每当内存回收的时候蓝色的线就会下降,但是存在部分内存一直得不到释放,所以蓝色的线逐渐升高

js错误引用DOM元素

    var nodes = '';
    (function () {
        var item = {
            name:new Array(1000000).join('x')
        }
        nodes = document.getElementById("nodes")
        nodes.item = item
        nodes.parentElement.removeChild(nodes)
    })()

这里的dom元素虽然已经从页面上移除了,但是js中仍然保存这对该dom元素的引用。
因为这段代码是只执行一次的,所以用timeline视图会很难分析出来是否存在内存泄漏,所以我们可以用 chrome dev tool 的 profile tab里的heap snapshot 工具来分析。
上面的代码贴一张 heap snapshot 的summary模式的截图
clipboard.png

通过constructor的filter功能,我们把上面代码中创建的长字符串找出来,可以看到代码运行结束后,内存中的长字符串依然没有被垃圾回收掉。
顺带提一下的是右边红框里的shadow size和 retainer size的含义

  • shadow size 指的是对象本地的大小

  • retainer size 指的是对象所引用内存的大小,回收该对象是会将他引用的内存也一并回收,所以retainer size 指代的是回收内存后会释放出来的内存大小

上面我们可以看到 长字符串本身的shadow size和retainer size是一样大的,这是引用长字符串没有引用其他的对象,如果有引用其他对象,那shadow size 和retainer size将不一致。

闭包循环引用

(function(){
    var theThing = null
    var replaceThing = function () {
        var originalThing = theThing
        var unused = function () {
            if (originalThing)
                console.log("hi")
        }
        theThing = {
            longStr: new Array(1000000).join('*'),
            someMethod: function someMethod() {
                console.log('someMessage')
            }
        };
    };
    setInterval(replaceThing,100)
})()

首先我们明确一下,unused是一个闭包,因为它引用了自由变量 originalThing,虽然它被没有使用,但v8引擎并不会把它优化掉,因为 JavaScript里存在eval函数,所以v8引擎并不会随便优化掉暂时没有使用的函数。

theThing 引用了someMethod,someMethod这个函数作用域隐式的和unused这个闭包共享一个闭包上下文。所以someMethod也引用了originalThing这个自由变量。

这里面的引用链是:

GCHandler -> replaceThing -> theThing -> someMethod -> originalThing -> someMethod(old) -> originalThing(older)-> someMethod(older)

随着setInterval的不断执行,这条引用链是不会断的,所以内存会不断泄漏,直致程序崩溃。
因为是闭包作用域引起的内存泄漏,这时候最好的选择是使用 chrome的heap snapshot的container视图,我们通过container视图能清楚的看到这条不断泄漏内存的引用链
clipboard.png

由于作者水平有限,文中如有错误还望指出,谢谢!

参考文档:

百科内存泄漏介绍
chrome devtolls
深入浅出nodejs
node-interview

查看原文

赞 37 收藏 46 评论 3

kukuv2 关注了标签 · 2017-03-01

weex

Weex 是阿里开源的一款跨平台移动开发工具,Weex 这个名字是取得 weeks 的谐音。

Weex能够完美兼顾性能与动态性,让移动开发者通过简捷的前端语法写出Native级别的性能体验,并支持iOS、安卓、YunOS及Web等多端部署。

对于移动开发者来说,Weex主要解决了频繁发版和多端研发两大痛点,同时解决了前端语言性能差和显示效果受限的问题。

开发者只需要在自己的APP中嵌入Weex的SDK,就可以通过撰写HTML/CSS/JavaScript来开发Native级别的Weex界面。Weex界面的生成码其实就是一段很小的JS,可以像发布网页一样轻松部署在服务端,然后在APP中请求执行。

与现有的开源跨平台移动开放项目如Facebook的React Native和微软的Cordova相比,Weex更加轻量,体积小巧。因为基于web conponent标准,使得开发更加简洁标准,方便上手。Native组件和API都可以横向扩展,方便根据业务灵活定制。Weex渲染层具备优异的性能表现,能够跨平台实现一致的布局效果和实现。对于前端开发来说,Weex能够实现组件化开发、自动化数据绑定,并拥抱Web标准。

http://alibaba.github.io/weex/

关注 1694

kukuv2 关注了问题 · 2017-01-07

解决用vue脚手架生成项目,控制台报错

图片描述
整个文件是用vue脚手架生成的,然后我安装了依赖,然后npm run dev 页面是空的,控制台报错。我也没有去修改这些文件夹

关注 3 回答 4

认证与成就

  • 获得 151 次点赞
  • 获得 6 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 5 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-08-25
个人主页被 2.6k 人浏览