前端常问

一、apply、call

apply:调用一个对象的一个方法,用另一个对象替换当前对象。例如:B.apply(A, arguments);即A对象应用B对象的方法。

call:调用一个对象的一个方法,用另一个对象替换当前对象。例如:B.call(A, args1,args2);即A对象调用B对象的方法。

callapply可以用来重新定义函数的执行环境,也就是this的指向;callapply都是为了改变某个函数运行时的context,即上下文而存在的,换句话说,就是为了改变函数体内部this的指向。

语法

call()

调用一个对象的方法,用另一个对象替换当前对象,可以继承另外一个对象的属性,它的语法是:

 

Function.call(obj[, param1[, param2[, [,...paramN]]]]);
  • obj:这个对象将代替Function类里this对象
  • params:一串参数列表

说明call方法可以用来代替另一个对象调用一个方法,call方法可以将一个函数的对象上下文从初始的上下文改变为obj指定的新对象,如果没有提供obj参数,那么Global对象被用于obj

apply()

call()方法一样,只是参数列表不同,语法:

 

Function.apply(obj[, argArray]);
  • obj:这个对象将代替Function类里this对象
  • argArray:这个是数组,它将作为参数传给Function

说明:如果argArray不是一个有效数组或不是arguments对象,那么将导致一个TypeError,如果没有提供argArrayobj任何一个参数,那么Global对象将用作obj

2.相同点

call()apply()方法的相同点就是这两个方法的作用是一样的。都是在特定的作用域中调用函数,等于设置函数体内this对象的值,以扩充函数赖以运行的作用域

一般来说,this总是指向调用某个方法的对象,但是使用call()apply()方法时,就会改变this的指向,看个例子:

 

function add(a, b) {
    return a + b;
}

function sub(a, b) {
    return a - b;
}

console.log(add.call(sub, 2, 1));//3

为什么add.call(sub, 2, 1)的执行结果是3呢,因为call()方法改变了this的指向,使得sub可以调用add的方法,也就是用sub去执行add中的内容,再来看一个例子:

 

function People(name, age) {
    this.name = name;
    this.age = age;
}

function Student(name, age, grade) {
    People.call(this, name, age);
    this.grade = grade;
}

var student = new Student('小明', 21, '大三');
console.log(student.name + student.age + student.grade);//小明21大三

在这个例子中,我们并没有给Studentnameage赋值,但是存在这两个属性的值,这还是要归功于call()方法,它可以改变this的指向。
在这个例子里,People.call(this, name, age);中的this代表的是Student,这也就是之前说的,使得Student可以调用People中的方法,因为People中有this.name = name;等语句,这样就将nameage属性创建到了Student中。

总结一句话就是call()可以让括号里的对象来继承括号外函数的属性

至于apply()方法作用也和call()方法一样,可以这么写:

 

People.apply(this, [name, age]);

或者这么写:

 

People.apply(this, arguments);

在这里arguments[name, age]是等价的。

3.不同点

从定义中也可以看出来,call()apply()的不同点就是接收参数的方式不同

  • apply()方法接收两个参数,一个是函数运行的作用域(this),另一个是参数数组。
  • call()方法不一定接受两个参数,第一个参数也是函数运行的作用域(this),但是传递给函数的参数必须列举出来。

在给对象参数的情况下,如果参数的形式是数组的时候,比如之前apply()方法示例里面传递了参数arguments,这个参数是数组类型,并且在调用Person的时候参数的列表是对应一致的(也就是PersonStudent的参数列表前两位是一致的)就可以采用apply()方法。

但是如果Person的参数列表是这样的(age,name),而Student的参数列表是(name,age,grade),这样就可以用call()方法来实现了,也就是直接指定参数列表对应值的位置Person.call(this,age,name)

4.拓展

apply()的其他用法

apply有一个巧妙的用处,就是可以将一个数组默认的转换为一个参数列表([param1,param2,param3]转换为param1,param2,param3),借助apply的这点特性,所以就有了以下高效率的方法:

1)Math.max可以实现得到数组中最大的一项

因为Math.max参数里面不支持Math.max([param1,param2]),也就是数组,但是它支持Math.max(param1,param2,param3…),所以可以根据apply的那个特点来解决:

 

var array = [1, 2, 3];
var max = Math.max.apply(null, array);
console.log(max);//3

这样轻易的可以得到一个数组中最大的一项,apply会将一个数组装换为一个参数接一个参数的传递给方法,这块在调用的时候第一个参数给了一个null,这个是因为没有对象去调用这个方法,我们只需要用这个方法帮我运算,得到返回的结果就行,所以直接传递了一个null过去,当然,第一个参数使用this也是可以的:

 

var array = [1, 2, 3];
var max = Math.max.apply(this, array);
console.log(max);//3

使用this就相当于用全局对象去调用Math.max,所以也是一样的。

2)Math.min可以实现得到数组中最小的一项

同样的Math.minMath.max是一个思想:

 

var array = [1, 2, 3];
var min = Math.min.apply(null, array);
console.log(min);//1

当然,apply的第一个参数可以用null也可以用this,这个是和Math.max一样的。

3)Array.prototype.push可以实现两个数组合并

同样的,push方法没有提供push一个数组,但是它提供了push(param1,param,…paramN)所以同样也可以通过apply来装换一下这个数组,即:

 

var arr1 = [1, 2, 3];
var arr2 = [4, 5, 6];
Array.prototype.push.apply(arr1, arr2);
console.log(arr1);//[ 1, 2, 3, 4, 5, 6 ]

可以这样理解,arr1调用了Arraypush方法,参数是通过apply将数组装换为参数列表的集合,其实,arr1也可以调用自己的push方法:

 

var arr1 = [1, 2, 3];
var arr2 = [4, 5, 6];
arr1.push.apply(arr1, arr2);
console.log(arr1);//[ 1, 2, 3, 4, 5, 6 ]

也就是只要有push方法,arr1就可以利用apply方法来调用该方法,以及使用apply方法将数组转换为一系列参数,所以也可以这样写:

 

var arr1 = [1, 2, 3];
var arr2 = [4, 5, 6];
[].push.apply(arr1, arr2);
console.log(arr1);//[ 1, 2, 3, 4, 5, 6 ]

总结

一般在目标函数只需要n个参数列表,但是不接收一个数组的形式([param1[,param2[,…[,paramN]]]]),我们就可以通过apply的方式来巧妙地解决。

 

二、防抖节流

防抖节流
目的
解决短时间内高频触发某事件导致响应速度跟不上频率,从而出现延迟、停顿、卡死等问题

防抖
1. 概念
高频触发事件后n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间

2. 实现
方式:每次触发事件时设置一个延迟调用方法,并且取消之前的延时调用方法,其中使用到闭包来缓存定时器

缺点:如果事件在规定的时间间隔内被不断的触发,则调用方法会被不断的延迟

3. 应用
登录、发短信时,用户点击多次,只发送一次请求
resize浏览器窗口变化,1秒内只重绘一次图表
文本编辑器实时保存,当无任何更改操作一秒后进行保存
/utils/methods:公共函数

/**
 * @description 函数防抖 (delay秒内只执行最后一次点击)
 * @param    {Function}  fn    要防抖的函数
 * @param    {Number}    delay 时间
 * @returns  {Function}
 * @constructor
 */
export const debounce = (fn, t) => {
  const delay = t || 500
  let timer
  return function () {
    const args = arguments
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      timer = null
      fn.apply(this, args)
    }, delay)
  }
}

// vue中使用
import { debounce } from '@/utils/methods'
  mounted() {
    // 监听滚动事件
    window.addEventListener('scroll', () => {
      const param = '传参'
      this.getData(param)
    })
  },
  methods: {
    // 定义防抖方法
    getData: debounce(function (val) { this.handle(val) }, 1000),
    handle (val) {
      console.error('防抖函数', val)
    },
  }


节流
1. 概念
触发高频事件后n秒内函数也只会执行一次,但如果一直被触发,则每n秒执行一次,相当于稀释函数的执行频率

2. 实现
2.1 定时器
方式:触发事件时,判断如果没有定时器,则延迟n秒执行函数且执行函数后将定时器清除,保证高频触发事件每n秒只执行1次

/**
 * @description 函数节流(每interval秒只执行一次)
 * @param     {Function}   fn       要节流的函数
 * @param    {Number}      interval 时间
 * @returns  {Function}
 * @constructor
 */
export const throttleTimer = (fn, t) => {
  const interval = t || 500
  let timer = null
  return function () {
    var context = this
    var args = arguments
    if (!timer) {
      timer = setTimeout(function () {
        fn.apply(context, args)
        timer = null
      }, interval)
    }
  }
}
// vue中使用
import { throttleTimer } from '@/utils/methods'
  mounted() {
    // 监听屏幕变化事件
    window.addEventListener('resize', () => {
      const param = '传参'
      this.getData(param)
    })
  },
  methods: {
    // 定义防抖方法
    getData: throttleTimer(function (val) { this.handle(val) }, 1000),
    handle (val) {
      console.error('节流函数', val)
    },
  }


2.2 时间戳
方式:触发事件时,如果当前时间和上一次触发时间差大于设定的高频时间间隔n,则立即执行函数,否则清除定时器,延迟n秒后执行函数/utils/methods:公共函数

/**
 * @description 函数节流(每interval秒只执行一次)
 * @param     {Function}   fn       要节流的函数
 * @param    {Number}      interval 时间
 * @returns  {Function}
 * @constructor
 */
export const throttleTimestamp = (fn, t) => {
  let last
  let timer
  const interval = t || 500
  return function () {
    const args = arguments
    const now = +new Date()
    if (last && now - last < interval) {
      clearTimeout(timer)
      timer = setTimeout(() => {
        last = now
        fn.apply(this, args)
      }, interval)
    } else {
      last = now
      fn.apply(this, args)
    }
  }
}

// vue中使用
import { throttleTimestamp } from '@/utils/methods'
  mounted() {
    // 监听屏幕变化事件
    window.addEventListener('resize', () => {
      const param = '传参'
      this.getData(param)
    })
  },
  methods: {
    // 定义防抖方法
    getData: throttleTimestamp(function (val) { this.handle(val) }, 1000),
    handle (val) {
      console.error('节流函数', val)
    },
  }


3. 应用
input输入框实时搜索:每隔1秒发送请求获取搜索列表
scroll滚动事件:每隔一秒计算一次位置信息
浏览器播放事件,每隔一秒计算进度条


三、webpack的一些常见配置(output  entry rules plugin)

1.mode开发模式

如果是development(开发模式),则打包后正常显示,如果是production(产品模式),则打包后为压缩模式。

2.入口文件(entry):

入口文件类似于其他语言的起始文件(如main函数所在的文件)。入口起点指示webpack应该使用哪个模块,来作为构建其内部依赖图的开始。进入入口起点后,webpack会找出有哪些模块和库时入口起点依赖的。

webpack配置的entry(有三种输入方式)

1.一个入口文件,所有的依赖全部在这个入口文件中指定:webpack 4.x后,一个入口文件默认为src下面的index.js,不用写entry

2.将两个平行的,不相依赖的两个文件打包在一起(以数组的形式):entry: ['./src/index.js','./src/a.js']

3.输出(output):

output属性告诉webpack在哪里输出它所创建的bundles,以及如何命名这些文件。

如果入口文件有多个,若output.filename还是写死的一个,它们之间就会进行报错。解决办法:通过一些占位符使每个文件输出名都是唯一的

①    [name]占位符

②    [hash]占位符

③    [chunkhash]占位符

4.loader

loader让webpack能够去处理那些非JS文件(webpack自身只理解JS)。loader可以将所有类型的文件转换为webpack能够处理的有效模块,然后就可以利用webpack的打包能力,对它们进行处理。

5.插件(plugins):

loader被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。插件的范围包括:从打包优化和压缩,一直到重新定义环境中的变量。插件的功能及其强大,可以用来处理各种各样的任务。

webpack的基础配置:

手动配置webpack:默认配置文件的名字为:webpack.config.js

//webpack是node写出来的  是node的写法

const path = require('path');
//const htmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    //模式默认有两种:development  production
    mode : 'development',
    entry: './src/index.js',   //入口
    output:{
        //路径必须是一个绝对路径,resolve可以把相对路径解析为一个绝对路径
        //path.resolve('dist')也可以,表示在当前路径下解析一个dist目录
        path : path.resolve(__dirname,'dist'),
        filename : '[name].bundle.js'   //打包后的文件名
    }

前端常问_第1张图片

如果想要修改默认的配置文件名称,如:把webpack.config.js文件名修改为webpack.config.my.js

当然直接执行npx webpack命令会报异常,找不到该配置文件,但是我们可以手动的指定配置文件,如下:

npx webpack --config webpack.config.my.js

前端常问_第2张图片

如果觉得这个名字太长了,每次执行起来很麻烦,我们可以到package.json文件中配置一些脚本

前端常问_第3张图片

执行命令npm run build即可

前端常问_第4张图片

webpack-dev-server:

webpack-dev-server是一个小型的Node.js Express服务器,它并不会真实的去打包文件,只是生成一个内存中的打包,把文件写到内存中,默认端口是8080。

安装:npm install webpack-dev-server -D

可以通过在package.json文件中添加一条配置:

"dev": "webpack-dev-server"

然后直接用npm run dev来执行命令

前端常问_第5张图片

 通过给定的地址直接访问页面即可

 

四、vue常见指令

 


五、微任务、宏任务


六、vuex中action和mutation为什么是异步、同步的

Vuex中所有的状态更新的唯一途径都是mutation,异步操作通过 Action 来提交 mutation实现,这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。

每个mutation执行完成后都会对应到一个新的状态变更,这样devtools就可以打个快照存下来,然后就可以实现 time-travel 了。如果mutation支持异步操作,就没有办法知道状态是何时更新的,无法很好的进行状态的追踪,给调试带来困难。


七、package.json是干什么的

package.json文件,定义了这个项目所需要的各种模块,以及项目的配置信息(比如名称、版本、许可证等元数据)

使用vue-cli脚手架新建的项目中,含有package.json。

package.json是npm的配置文件,里面设定了脚本以及项目依赖的库。 npm run dev 这样的命令就写在package.json里。

{ 
 "name": "vue-manage", // 项目名称 
 "version": "1.0.0", // 版本  
 "description": "Reimbursement Manage", // 描述  
 "author": "LXG", // 作者  
 "private": true, //是否私人项目  
 "scripts": { 
  "dev": "node build/dev-server.js", // npm run dev 的 dev,使用node执行 build/dev-server.js 
  "start": "node build/dev-server.js", // npm run start 跑的是同样的命令 
  "build": "node build/build.js", // npm run build 跑的是 node build/build.js // 以下脚本为单元测试用到的脚本 
  // 以下脚本为单元测试用到的脚本 
  "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run", 
  "e2e": "node test/e2e/runner.js", 
  "test": "npm run unit && npm run e2e"
 }, 
 "dependencies": { // dependencies 设定的是项目里使用的依赖 
  "vue": "^2.2.6", 
  "vue-router": "^2.3.1", 
  "element-ui": "1.3.1", 
  "vue-datasource": "1.0.9", 
  "axios": "^0.15.3", 
  "vue-core-image-upload": "2.1.5", 
  "mockjs": "^1.0.1-beta3", 
  "babel-polyfill": "^6.23.0"
 }, 
 "devDependencies": { //devDependencies设定的是开发使用的依赖  
  "autoprefixer": "^6.7.2", // 是用于给css3属性自动加属性前缀的 
  "babel-core": "^6.22.1", // babel相关的都是用于处理es6语法的 
  "babel-loader": "^6.2.10", 
  "babel-plugin-transform-runtime": "^6.22.0", 
  "babel-preset-env": "^1.3.2", 
  "babel-preset-stage-2": "^6.22.0", 
  "babel-register": "^6.22.0", 
  "chalk": "^1.1.3", // chalk适用于格式化输出命令行信息的,比如run dev以后的start...  
  "connect-history-api-fallback": "^1.3.0", 
  "copy-webpack-plugin": "^4.0.1", 
  "css-loader": "^0.28.0", // 所有的*-loader都是 webpack的扩展,webpack是把各种资源理解为一个模块,css-loader就是读取css模块的加载器 
  "eslint": "^3.19.0", // eslint 相关是代码格式化检查工具,开启以后要严格遵照它规定的格式进行开发 (我一般把它关了,为了显示特意加上的) 
  "eventsource-polyfill": "^0.9.6", 
  "express": "^4.14.1", // express 用于启动 node http server的服务  
  "extract-text-webpack-plugin": "^2.0.0", 
  "file-loader": "^0.11.1", 
  "friendly-errors-webpack-plugin": "^1.1.3", 
  "html-webpack-plugin": "^2.28.0", // webpack 里载入和处理html的插件 
  "http-proxy-middleware": "^0.17.3", // node server 的中间件工具 
  "webpack-bundle-analyzer": "^2.2.1", 
  "cross-env": "^4.0.0", // 设定环境变量的工具,NODE_ENV变量跟它有关 
  "karma": "^1.4.1",  // karma相关的都是单元测试工具 
  "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.30", 
  "karma-webpack": "^2.0.2", 
  "lolex": "^1.5.2", 
  "mocha": "^3.2.0", 
  "chai": "^3.5.0", 
  "sinon": "^2.1.0", 
  "sinon-chai": "^2.8.0", 
  "inject-loader": "^3.0.0", 
  "babel-plugin-istanbul": "^4.1.1", 
  "phantomjs-prebuilt": "^2.1.14", 
  "chromedriver": "^2.27.2", 
  "cross-spawn": "^5.0.1", 
  "nightwatch": "^0.9.12", 
  "selenium-server": "^3.0.1", // 一个版本检查工具  
  "semver": "^5.3.0", // selljs是在node里跑shell命令的工具,比如‘rm -rf'  
  "shelljs": "^0.7.6", 
  "opn": "^4.0.2", // 跨平台的开启文件或者网页的工具 
  "optimize-css-assets-webpack-plugin": "^1.3.0", 
  "ora": "^1.2.0", // 命令行里自动运行的信息提示  
  "rimraf": "^2.6.0", // 跑shell命令 rm-rf 的工具  
  "url-loader": "^0.5.8", // 配合webpack的加载器  
  "vue-loader": "^11.3.4", // 配合webpack的加载器  
  "vue-style-loader": "^2.0.5", // 配合webpack的加载器  
  "vue-template-compiler": "^2.2.6", // vue-template-compiler,可能是配合离线版vue  
  "webpack": "^2.3.3", // webpack相关的用于,把图片,*.vue, *.js, 这些组合成最终的项目,webpack-dev用于跑测试服务器  
  "webpack-dev-middleware": "^1.10.0", 
  "webpack-hot-middleware": "^2.18.0", 
  "webpack-merge": "^4.1.0", 
  "babel-preset-es2015": "^6.22.0", 
  "function-bind": "^1.1.0", 
  "webpack-bundle-analyzer": "^2.2.1"  
 },  // 项目依赖的引擎版本  
 "engines": { 
  "node": ">= 4.0.0", 
  "npm": ">= 3.0.0"
 }, 
 "browserslist": [ 
  "> 1%", 
  "last 2 versions", 
  "not ie <= 8"
 ] 
}

scripts:支持的脚本,默认是一个空的test
dependencies: 生产环境依赖包列表
devDependencies: 开发环境、测试环境依赖包列表
engines: 声明项目需要的node或npm版本范围

八、peerDependencies

有时,你的项目和所依赖的模块,都会同时依赖另一个模块,但是所依赖的版本不一样。比如,你的项目依赖A模块和B模块的1.0版,而A模块本身又依赖B模块的2.0版。

大多数情况下,这不构成问题,B模块的两个版本可以并存,同时运行。但是,有一种情况,会出现问题,就是这种依赖关系将暴露给用户。

最典型的场景就是插件,比如A模块是B模块的插件。用户安装的B模块是1.0版本,但是A插件只能和2.0版本的B模块一起使用。这时,用户要是将1.0版本的B的实例传给A,就会出现问题。因此,需要一种机制,在模板安装的时候提醒用户,如果A和B一起安装,那么B必须是2.0模块。

peerDependencies字段,就是用来供插件指定其所需要的主工具的版本。

{
  "name": "chai-as-promised",
  "peerDependencies": {
    "chai": "1.x"
  }
}

上面代码指定,安装chai-as-promised模块时,主程序chai必须一起安装,而且chai的版本必须是1.x。如果你的项目指定的依赖是chai的2.0版本,就会报错。

注意,从npm 3.0版开始,peerDependencies不再会默认安装了。

 

 

 

 


九、http状态码


十、cookie是干什么的


十一、父子组件生命周期的顺序

先附一张官网上的vue实例的生命周期图,每个Vue实例在被创建的时候都需要经过一系列的初始化过程,例如需要设置数据监听,编译模板,将实例挂载到DOM并在数据变化时更新DOM等。同时在这个过程中也会运行一些叫做生命周期钩子的函数(回调函数),这给了用户在不同阶段添加自己代码的机会。

1、vue的生命周期图

 

前端常问_第6张图片

在vue实例的整个生命周期的各个阶段,会提供不同的钩子函数以供我们进行不同的操作。先列出vue官网上对各个钩子函数的详细解析。

生命周期钩子    

详细
beforeCreate 在实例初始化之后,数据观测(data observer) 和 event/watcher 事件配置之前被调用。
created

实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调。

在执行data()方法前props属性有数据已经可以访问,watch和computed监听函数此时为null,此时this.computed里的计算属性值为undefined。data函数执行完后,watch和computed监听函数才可用,因为data函数执行完后,data函数return的属性这时才可用。然而,挂载阶段还没开始,$el 属性目前不可见。

beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用。
mounted el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。如果 root 实例挂载了一个文档内元素,当 mounted 被调用时 vm.$el 也在文档内。
beforeUpdate 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。你可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。
updated 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。
activated keep-alive 组件激活时调用。
deactivated keep-alive 组件停用时调用。
beforeDestroy 实例销毁之前调用。在这一步,实例仍然完全可用。
destroyed Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。

2、实际操作 

下面我们在实际的代码执行过程中理解父子组件生命周期创建过程以及钩子函数执行的实时状态变化。

测试基于下面的代码,引入vue.js文件后即可执行。(打开页面后,再按一次刷新会自动进入debugger状态)




    
    
    
    Document
    
   

{{message}}

复制代码

3.1、生命周期调试

首先我们创建了一个Vue实例vm,将其挂载到页面中id为“app”的元素上。

3.1.1、根组件的beforeCreate阶段

前端常问_第7张图片

可以看出,在调用beforeCreate()函数时,只进行了一些必要的初始化操作(例如一些全局的配置和根实例的一些属性初始化),此时data属性为undefined,没有可供操作的数据。

3.1.2、根组件的Created阶段

前端常问_第8张图片

调用Created()函数,在这一步,实例已完成以下的配置:数据代理和动态数据绑定(data observer),属性和方法的运算, watch/event 事件回调。然而,挂载阶段还没开始,$el 属性目前不可见。

3.1.3、根组件的beforeMount阶段

前端常问_第9张图片

前端常问_第10张图片

在调用boforeMount()函数前首先会判断对象是否有el选项。如果有的话就继续向下编译,如果没有el选项,则停止编译,也就意味着停止了生命周期,直到在该vue实例上调用vm.$mount(el)

在这个例子中,我们有el元素,因此会调用boforeMount()函数,此时已经开始执行模板解析函数,但还没有将$el元素挂载页面,页面视图因此也未更新。在标红处,还是 {{message}},这里就是应用的 Virtual DOM(虚拟Dom)技术,先把坑占住了。到后面mounted挂载的时候再把值渲染进去。

3.1.4、子组件的beforeCreate、Created、beforeMount、Mounted阶段

在父组件执行beforeMount阶段后,进入子组件的beforeCreate、Created、beforeMount阶段,这些阶段和父组件类似,按下不表。beforeMount阶段后,执行的是Mounted阶段,该阶段时子组件已经挂载到父组件上,并且父组件随之挂载到页面中。

由下图可以知道,在beforeMount阶段之后、Mounted阶段之前,数据已经被加载到视图上了,即$el元素被挂载到页面时触发了视图的更新。

前端常问_第11张图片

3.1.5、子组件的activated阶段

 我们发现在子父组件全部挂载到页面之后被触发。这是因为子组件my-components被 包裹,随$el的挂载被触发。如果子组件没有被包裹,那么该阶段将不会被触发。

前端常问_第12张图片

3.1.6、父组件的mounted阶段

mounted执行时:此时el已经渲染完成并挂载到实例上。

至此,从Vue实例的初始化到将新的模板挂载到页面上的阶段已经完成,退出debugger。下面我们来看一下deactivated、beforeUpdate、updated、beforeDestroy、destroyed钩子函数。

3.2、deactivated、beforeUpdate、updated阶段

由生命周期函数可知:当数据变化后、虚拟DOM渲染重新渲染页面前会触发beforeUpdate()函数,此时视图还未改变。当虚拟DOM渲染页面视图更新后会触发updated()函数。

前端常问_第13张图片

 

我们不妨改变vm.show = false,当修改这个属性时,不仅会触发beforeUpdate、updated函数,还会触发deactivated函数(因为keep-alive 组件停用时调用)。我们不妨想一下deactivated函数会在beforeUpdate后还是updated后调用。

我们在控制台输入vm.show = false。得到三者的调用顺序分别为beforeUpdate、deactivated、updated。我们可以知道的是deactivated函数的触发时间是在视图更新时触发。因为当视图更新时才能知道keep-alive组件被停用了。

前端常问_第14张图片

前端常问_第15张图片

3.3、beforeDestroy和destroyed钩子函数间的生命周期

现在我们对Vue实例进行销毁,调用app.$destroy()方法即可将其销毁,控制台测试如下:

前端常问_第16张图片

我们发现实例依然存在,但是此时变化已经发生在了其他地方。

beforeDestroy钩子函数在实例销毁之前调用。在这一步,实例仍然完全可用。

destroyed钩子函数在Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁(也就是说子组件也会触发相应的函数)。这里的销毁并不指代'抹去',而是表示'解绑'。

销毁时beforeDestory函数的传递顺序为由父到子,destory的传递顺序为由子到父。

4、一些应用钩子函数的想法

  • 在created钩子中可以对data数据进行操作,这个时候可以进行ajax请求将返回的数据赋给data。
  • 虽然updated函数会在数据变化时被触发,但却不能准确的判断是那个属性值被改变,所以在实际情况中用computed或match函数来监听属性的变化,并做一些其他的操作。
  • 在mounted钩子对挂载的dom进行操作,此时,DOM已经被渲染到页面上。
  • 在使用vue-router时有时需要使用来缓存组件状态,这个时候created钩子就不会被重复调用了,如果我们的子组件需要在每次加载或切换状态的时候进行某些操作,可以使用activated钩子触发。
  • 所有的生命周期钩子自动绑定 this 上下文到实例中,所以不能使用箭头函数来定义一个生命周期方法 (例如 created: () => this.fetchTodos())。这是导致this指向父级。

5、 小结

  • 加载渲染过程

  父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted

  • 子组件更新过程

  父beforeUpdate->子beforeUpdate->子updated->父updated

  • 父组件更新过程

  父beforeUpdate->父updated

  • 销毁过程

  父beforeDestroy->子beforeDestroy->子destroyed->父destroyed

6、参考文章

关于Vue.js2.0生命周期的研究与理解

Vue2.0 探索之路——生命周期和钩子函数的一些理解

 

十二、在Vue生命周期的beforeUpdate阶段中,修改data里的数组,为什么会引起死循环?

百度到的网友答案:

一般beforeupdate 里不建议进行这类数据的修改,因为update执行前就以及形成了他的virtual DOM,DOM准备渲染的时候,参数也进行修改,容易出现了死循环。就是在beforeupdate的时候不能进行数据和虚拟DOM的修改,不然就会造成 ‘更新数据的时间去更新数据’ 必然出现死循环

 

十三、computed和watch的区别、用法

computed     

    当一个属性受多个属性影响的时候就需要用到computed

    最典型的例子: 购物车商品结算的时候

watch

    当一条数据影响多条数据的时候就需要用watch

    搜索数据

 

十四、cookie属性详解

在chrome控制台中的resources选项卡中可以看到cookie的信息。

一个域名下面可能存在着很多个cookie对象。

name  字段为一个cookie的名称。

value  字段为一个cookie的值。

domain  字段为可以访问此cookie的域名。

非顶级域名,如二级域名或者三级域名,设置的cookie的domain只能为顶级域名或者二级域名或者三级域名本身,不能设置其他二级域名的cookie,否则cookie无法生成。

顶级域名只能设置domain为顶级域名,不能设置为二级域名或者三级域名,否则cookie无法生成。

二级域名能读取设置了domain为顶级域名或者自身的cookie,不能读取其他二级域名domain的cookie。所以要想cookie在多个二级域名中共享,需要设置domain为顶级域名,这样就可以在所有二级域名里面或者到这个cookie的值了。
顶级域名只能获取到domain设置为顶级域名的cookie,其他domain设置为二级域名的无法获取。

path  字段为可以访问此cookie的页面路径。 比如domain是abc.com,path是/test,那么只有/test路径下的页面可以读取此cookie。

expires/Max-Age   字段为此cookie超时时间。若设置其值为一个时间,那么当到达此时间后,此cookie失效。不设置的话默认值是Session,意思是cookie会和session一起失效。当浏览器关闭(不是浏览器标签页,而是整个浏览器) 后,此cookie失效。

Size  字段 此cookie大小。

http  字段  cookie的httponly属性。若此属性为true,则只有在http请求头中会带有此cookie的信息,而不能通过document.cookie来访问此cookie。

secure   字段 设置是否只能通过https来传递此条cookie

十五、Vue 常用指令

v-html

v-text

v-for

v-if

v-show

十六、v-if和v-show的性能比较

我们简单比较一下二者的区别:

实现方式:v-if底层采用的是appendChild来实现的,v-show通过样式的display控制标签的显示,正因为实现方式上面有差异,导致了他们的加载速度方面产生了差异;

加载性能:v-if加载速度更快,v-show加载速度慢

切换开销:v-if切换开销大,v-show切换开销小

v-if是惰性的,它是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建,v-show 也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。

v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。

一般来说,v-if有更高的切换开销,而v-show有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用v-show较好,如果在运行时条件很少改变,则使用v-if较好。

v-bind

v-on

v-model

十七、localStorage 和 sessionStorage

sessionStorage 用于临时保存同一窗口(或标签页)的数据,在关闭窗口或标签页之后将会删除这些数据。

提示: 如果你想在浏览器窗口关闭后还保留数据,可以使用 localStorage 属性, 该数据对象没有过期时间,今天、下周、明年都能用,除非你手动去删除。

十八、导航守卫(路由守卫)

导航守卫一共有三种形式

1.全局导航守卫用的时候写在主文件中(main.js),全局的

beforeEach

afterEach

2.路由独享守卫

beforeEnter(to,from,next)

3.组件内守卫

beforeRouteEnter((to,from,next)=>{})

beforeRouteUpdate ( to,from,next ) { //动态路由用 console.log('路由改变了') next() }

beforeRouteLeave (to, from, next) {}

十九、路由按需加载

import()

使用import关键字引入,这个import(‘XXX’)函数会返回一个Promise对象;参考资料vue-router路由懒加载

{
  path: '/',
  name: 'index',
  component:() => import('@/views/index/index')
 }

二十、数组拼接

1.ES6数组解构(解构是浅拷贝)

这个方法不会改变原数组的内容,返回新数组。

前端常问_第17张图片

2.concat方法

前端常问_第18张图片

3.for循环

遍历其中一个数组,把该数组中的所有元素依次添加到另外一个数组中。直接上代码:

for( var i in b)

{

  a.push(b[i]);

}

这样的写法可以解决第一种方案中对内存的浪费,但是会有另一个问题:丑!这么说不是没有道理,如果能只用一行代码就搞定,岂不快哉~

4.通过map()

arr1.map(item=>{
    arr2.push(item) 
 });
  console.log(arr2)  // [1, 2, 3, "a", "b", "c", "d", "e", "f"]
这样写性能相对来说要高一点,但是也会改变数组本身的值,这样看起来逼格高一点啦~~~

5.apply

函数的apply方法有一个特性,那就是func.apply(obj,argv),argv是一个数组。所以我们可以利用这点,直接上代码:

1.
arr1.push.apply(arr1,arr2);   
console.log(arr1)  // [1, 2, 3, "a", "b", "c", "d", "e", "f"] 
2.
Array.prototype.push.apply(arr1,arr2);
console.log(arr1)  // [1, 2, 3, "a", "b", "c", "d", "e", "f"]

调用arr1.push这个函数实例的apply方法,同时把,arr2当作参数传入,这样arr1.push这个方法就会遍历arr2数组的所有元素,达到合并的效果。也会改变数组本身的值

二十一、ES6

二十二、methods方法和computed计算属性

1).methods方法和computed计算属性,两种方式的最终结果确实是完全相同

2).不同的是计算属性是基于它们的响应式依赖进行缓存的。

    只在相关响应式依赖发生改变时它们才会重新求值,多次访问 getAge 计算属性会立即返回之前的计算结果,而不必再次执行函数。

3).methods方法,每当触发重新渲染时,调用方法将总会再次执行函数。

4).官网的一句话:对于任何复杂逻辑,你都应当使用计算属性。

二十三、deep

使用   deep   深度监听  (对象里面的属性值发生改变)

当需要监听一个对象的改变时,普通的watch方法无法监听到对象内部属性的改变,只有data中的数据才能够监听到变化,此时就需要deep属性对对象进行深度监听。


new Vue({
  el: '#root',
  data: {
    cityName: {id: 1, name: 'shanghai'}
  },
  watch: {
    cityName: {
      handler(newName, oldName) {      // ...    },
    deep: true,
    immediate: true
    }
  } 
})
设置deep: true 则可以监听到cityName.name的变化,此时会给cityName的所有属性都加上这个监听器,当对象属性较多时,每个属性值的变化都会执行handler。如果只需要监听对象中的一个属性值,则可以做以下优化:使用字符串的形式监听对象属性:
watch: {
'cityName.name': {
      handler(newName, oldName) { 
     // ...    
  },
      deep: true,
      immediate: true
    }
  }

这样就只会给对象的某个特定的属性加监听器。

数组(一维、多维)的变化不需要通过深度监听,对象数组中对象的属性变化则需要deep深度监听。

二十四、组件间传参

二十五、有没有对element-ui组件进行过二次封装

二十六、项目性能优化

二十七、vue组件中data为什么必须是个函数

组件中的data写成一个函数,数据以函数返回值形式定义,这样每复用一次组件,就会返回一份新的data,类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据。而单纯的写成对象形式,就使得所有组件实例共用了一份data,就会造成一个变了全都会变的结果。

二十八、ES6拼接数组的方法

二十九、针对数组的哪几个操作会触发数据双向绑定

前端常问_第19张图片

三十、事件委托的优点

三十一、使用vuex的优点在哪?(相对于其它的数据存储方法,如:本地储存localStorage...)
三十二、浏览器缓存原理(强缓存、协议缓存)

三十四、js的执行顺序(宏任务、微任务)

三十五、怎么做权限控制

三十六、js绑定事件的方法

三十七、能说一下什么是原型链吗?

三十八、怎么是一个弹窗居中

三十九、怎么实现路由按需加载

四十、你在项目中做过哪些优化

四十一、请说一响应式数据的原理

核心点:object.defineProperty

默认vue在初始化数据时,会给data中的属性使用object.defineProperty重新定义所有的属性,当页面取到相应的属性时,会进行依赖收集(收集当前组件的watcher)如果属性发生变化会通知相关依赖进行更新操作。

原理图如下:

前端常问_第20张图片

四十二、vue中是如何检测数组变化?

不是通过object.defineProperty

前端常问_第21张图片

shift() 方法用于把数组的第一个元素从其中删除,并返回第一个元素的值。

pop()   移除数组的最后一项,返回移除的项

unshift()  在数组的第一项前面添加一个或多个元素,返回数组的长度

改写了这7个方法

前端常问_第22张图片

四十三、为何vue采用异步渲染

前端常问_第23张图片

四十四、nextTick的原理

微任务、宏任务(异步方法)

前端常问_第24张图片

四十五、vue组件的生命周期

前端常问_第25张图片

前端常问_第26张图片

四十六、ajax请求放在哪个生命周期中

四十七、VUE中模板编译原理

前端常问_第27张图片

四十八、为什么v-for和v-if不能连用

v-for比v-if优先级更高

四十九、什么叫虚拟节点

五十、v-for为什么要用key

因为diff算法

 

五十一、Vue客户端渲染和服务端渲染异同

这里不讲Vue服务端渲染的具体实现过程,需要学习服务端渲染的同学可以异步Vue服务端渲染官方教程地址: https://ssr.vuejs.org/,相信会比一般博客讲的更为清楚。官方教程同时也提供了Demo,地址如下:https://github.com/vuejs/vue-hackernews-2.0/ ,该demo功能齐全,需要的小伙伴可以直接clone下来使用。 

如果想要使用更高层的服务渲染框架,可以了解下Nuxt.js,连接地址如下:https://nuxtjs.org/ 。上面几个地址相信对想要学习或了解Vue服务端渲染的同学很有帮助,下面就开始讲下Vue客户端渲染和服务端渲染的异同和一些概念。

1、客户端渲染。
Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。什么意思呢?就是我们的页面开始是没内容的,加载js后,js会生成和操纵dom,最后又浏览器渲染出页面。这一系列的操作都是在浏览器完成的。

看下实例:Vue客户端应用加载时如下:


url(http://localhost:8080/#/) 加载出来的是一个空的页面,该页面加载了app.js这个文件,该js文件会产生和操作Dom,最终浏览器渲染和绘制页面。最终页面如下:

2、服务端渲染
服务端渲染其实很好理解,浏览器请求的页面就是服务器渲染好的。在前后端不分离的时代(现在依然有很多公司这样做),很多同学应该都知道Freemarker等模板,就是将页面中的变量替换成实际数据之后,再交给浏览器渲染。
看一下服务端渲染的例子:

很明显,客户端接收的文件是服务端已经渲染过的,即url(http://localhost:8080/user/123) 获取的已经是一个可展现的页面,这点就是服务端和客户端渲染的最大区别。服务端渲染的结果如下:


既然可以在客户端渲染,为什么还要有服务端渲染呢,进行服务端渲染一般会有下年几方面原因:

更好的SEO(搜索引擎优化,即让搜索引擎能多的搜索网站) 

现在,大部分搜索引擎都已经能够索引同步JavaScript程序或应用,注意是同步。如果网站通过Ajax异步加载内容,并渲染到页面上,搜索引擎是无法感知的,所以这个时候服务端渲染时一个更好的选择。
更快的内容到达时间

在网速比较慢,设备性能比较低时,使用服务端渲染是一个比较好的选择,服务端把完整的页面交给浏览器,浏览器只需渲染即可。从用户体验的角度考虑,这时服务端渲染是一个更好的选择。

当然可能还包括,开发条件受限,有些类库必须使用服务渲染。一些安装部署的特殊要求等等。

服务端渲染会单纯的静态资源服务器相比,会损耗更多的CPU资源,所以设计好相应的缓存策略很重要,在使用服务端渲染前要考虑好是否真的需要进行服务端渲染。

五十二、浏览器渲染原理

一、浏览器如何渲染网页

概述:浏览器渲染一共有五步

  1. 处理 HTML 并构建 DOM 树。
  2. 处理 CSS构建 CSSOM 树。
  3. DOMCSSOM 合并成一个渲染树。
  4. 根据渲染树来布局,计算每个节点的位置。
  5. 调用 GPU 绘制,合成图层,显示在屏幕上

第四步和第五步是最耗时的部分,这两步合起来,就是我们通常所说的渲染

具体如下图过程如下图所示

 

前端常问_第28张图片

前端常问_第29张图片

渲染

  • 网页生成的时候,至少会渲染一次
  • 在用户访问的过程中,还会不断重新渲染

重新渲染需要重复之前的第四步(重新生成布局)+第五步(重新绘制)或者只有第五个步(重新绘制)

  • 在构建 CSSOM 树时,会阻塞渲染,直至 CSSOM树构建完成。并且构建 CSSOM 树是一个十分消耗性能的过程,所以应该尽量保证层级扁平,减少过度层叠,越是具体的 CSS 选择器,执行速度越慢
  • HTML 解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方重新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件。并且CSS也会影响 JS 的执行,只有当解析完样式表才会执行 JS,所以也可以认为这种情况下,CSS 也会暂停构建 DOM

二、浏览器渲染五个阶段

2.1 第一步:解析HTML标签,构建DOM树

在这个阶段,引擎开始解析html,解析出来的结果会成为一棵dom
dom的目的至少有2

  • 作为下个阶段渲染树状图的输入
  • 成为网页和脚本的交互界面。(最常用的就是getElementById等等)

当解析器到达script标签的时候,发生下面四件事情

  1. html解析器停止解析,
  2. 如果是外部脚本,就从外部网络获取脚本代码
  3. 将控制权交给js引擎,执行js代码
  4. 恢复html解析器的控制权

由此可以得到第一个结论1

  • 由于
    • 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)
    • DOM 离线后修改,比如:先把 DOMdisplay:none (有一次 Reflow),然后你修改100次,然后再把它显示出来
    • 不要把 DOM 结点的属性值放在一个循环里当成循环里的变量

     

    for(let i = 0; i < 1000; i++) {
        // 获取 offsetTop 会导致回流,因为需要去获取正确的值
        console.log(document.querySelector('.test').style.offsetTop)
    }
    
    • 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
    • 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame
    • CSS选择符从右往左匹配查找,避免 DOM深度过深
    • 将频繁运行的动画变为图层,图层能够阻止该节点回流影响别的元素。比如对于 video标签,浏览器会自动将该节点变为图层。

     

    前端常问_第31张图片

    重绘与回流 http://blog.poetries.top/2018/01/12/fed-performance-optimization/#%E5%85%AD%E3%80%81%E9%87%8D%E7%BB%98%E4%B8%8E%E5%9B%9E%E6%B5%81

    五十三、const 声明对象或者数组可以改变吗

    前端常问_第32张图片

    为什么const 定义的对象和数组可以改变它的值呢?

    对象和数组是引用类型,a中保存的仅是数组的指针,这就意味着,const仅保证指针不发生改变,修改数组的值不会改变对象的指针,所以是被允许的。也就是说const定义的引用类型只要指针不发生改变,其他的不论如何改变都是允许的。

    我们试着修改一下指针,让a指向一个新数组,结果如下图

    前端常问_第33张图片

    即使对象的内容没发生改变,指针改变也是不允许的。

    五十四、JavaScript中Map和ForEach的区别

    定义

    MDN上对Map和ForEach的定义:

    • forEach(): 针对每一个元素执行提供的函数。
    • map(): 创建一个新的数组,其中每一个元素由调用数组中的每一个元素执行提供的函数得来。

    到底有什么区别呢?

    forEach()方法不会返回执行结果,而是undefined

    也就是说,forEach()会修改原来的数组。而map()方法会得到一个新的数组并返回。

    执行速度对比

    前端常问_第34张图片

    可以看到,在我到电脑上forEach()的执行速度比map()慢了70%。每个人的浏览器的执行结果会不一样。你可以使用下面的链接来测试一下: Map vs. forEach - jsPref

    适用场合

    取决于你想要做什么。

    forEach适合于你并不打算改变数据的时候,而只是想用数据做一些事情 – 比如存入数据库或则打印出来。

    let arr = ['a', 'b', 'c', 'd'];
    arr.forEach((letter) => {
        console.log(letter);
    });
    // a
    // b
    // c
    // d

    map()适用于你要改变数据值的时候。不仅仅在于它更快,而且返回一个新的数组。这样的优点在于你可以使用复合(composition)(map(), filter(), reduce()等组合使用)来玩出更多的花样。

    let arr = [1, 2, 3, 4, 5];
    let arr2 = arr.map(num => num * 2).filter(num => num > 5);
    // arr2 = [6, 8, 10]

    上面代码中:我们首先使用map将每一个元素乘以2,然后紧接着筛选出那些大于5的元素。最终结果赋值给arr2

    总结

    • 能用forEach()做到的,map()同样可以。反过来也是如此。
    • map()会分配内存空间存储新数组并返回,forEach()不会返回数据。
    • forEach()允许callback更改原始数组的元素。map()返回新的数组。

    五十五、for(var i=0;i<3;i++){ setTimeout(function() { console.log(i) }, 10);}

    前端常问_第35张图片

    这道题涉及了异步、作用域、闭包

    settimeout是异步执行,10ms后往任务队列里面添加一个任务,只有主线上的全部执行完,才会执行任务队列里的任务,当主线执行完成后,i是3,所以此时再去执行任务队列里的任务时,i全部是3了。对于打印3次是:

     每一次for循环的时候,settimeout都执行一次,但是里面的函数没有被执行,而是被放到了任务队列里面,等待执行,for循环了3次,就放了3次,当主线程执行完成后,才进入任务队列里面执行。

       (注意:for循环从开始到结束的过程,需要维持几微秒或几毫秒。)

    当我把var 变成let 时

    for(let i=0;i<3;i++){ setTimeout(function() {  console.log(i)  }, 10);}

    打印出的是:0,1,2

    当解决变量作用域,

    因为for循环头部的let不仅将i绑定到for循环快中,事实上它将其重新绑定到循环体的每一次迭代中,确保上一次迭代结束的值重新被赋值。setTimeout里面的function()属于一个新的域,通过 var 定义的变量是无法传入到这个函数执行域中的,通过使用 let 来声明块变量,这时候变量就能作用于这个块,所以 function就能使用 i 这个变量了;这个匿名函数的参数作用域 和 for参数的作用域 不一样,是利用了这一点来完成的。这个匿名函数的作用域有点类似类的属性,是可以被内层方法使用的。

    五十六、JS中的let和var的区别

    其实阮一峰老师的ES6中已经很详细介绍了let的用法和var的区别。我简单总结一下,以便各位以后面试中使用。

    ES6 新增了let命令,用来声明局部变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效,而且有暂时性死区的约束。

    先看个var的常见变量提升的面试题目:

    题目1:
    var a = 99;            // 全局变量a
    f();                   // f是函数,虽然定义在调用的后面,但是函数声明会提升到作用域的顶部。 
    console.log(a);        // a=>99,  此时是全局变量的a
    function f() {
      console.log(a);      // 当前的a变量是下面变量a声明提升后,默认值undefined
      var a = 10;
      console.log(a);      // a => 10
    }
    
    // 输出结果:
    undefined
    10
    99
    

    如果以上题目有理解困难的童鞋,请系统的看一下老马的免费JS高级视频教程。

    ES6可以用let定义块级作用域变量

    在ES6之前,我们都是用var来声明变量,而且JS只有函数作用域和全局作用域,没有块级作用域,所以{}限定不了var声明变量的访问范围。
    例如:

    { 
      var i = 9;
    } 
    console.log(i);  // 9
    

    ES6新增的let,可以声明块级作用域的变量。

    { 
      let i = 9;     // i变量只在 花括号内有效!!!
    } 
    console.log(i);  // Uncaught ReferenceError: i is not defined
    

    let 配合for循环的独特应用

    let非常适合用于 for循环内部的块级作用域。JS中的for循环体比较特殊,每次执行都是一个全新的独立的块作用域,用let声明的变量传入到 for循环体的作用域后,不会发生改变,不受外界的影响。看一个常见的面试题目:

    for (var i = 0; i <10; i++) {  
      setTimeout(function() {  // 同步注册回调函数到 异步的 宏任务队列。
        console.log(i);        // 执行此代码时,同步代码for循环已经执行完成
      }, 0);
    }
    // 输出结果
    10   共10个
    // 这里面的知识点: JS的事件循环机制,setTimeout的机制等
    

    如果把 var改成 let声明:

    // i虽然在全局作用域声明,但是在for循环体局部作用域中使用的时候,变量会被固定,不受外界干扰。
    for (let i = 0; i < 10; i++) { 
      setTimeout(function() {
        console.log(i);    //  i 是循环体内局部作用域,不受外界影响。
      }, 0);
    }
    // 输出结果:
    0  1  2  3  4  5  6  7  8 9
    

    let没有变量提升与暂时性死区

    let声明的变量,不存在变量提升。而且要求必须 等let声明语句执行完之后,变量才能使用,不然会报Uncaught ReferenceError错误。
    例如:

    console.log(aicoder);    // 错误:Uncaught ReferenceError ...
    let aicoder = 'aicoder.com';
    // 这里就可以安全使用aicoder
    

    ES6 明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
    总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。

    let变量不能重复声明

    let不允许在相同作用域内,重复声明同一个变量。否则报错:Uncaught SyntaxError: Identifier 'XXX' has already been declared

    例如:

    let a = 0;
    let a = 'sss';
    // Uncaught SyntaxError: Identifier 'a' has already been declared
    

    总结

    ES6的let让js真正拥有了块级作用域,也是向这更安全更规范的路走,虽然加了很多约束,但是都是为了让我们更安全的使用和写代码。

    五十七、webpack如何引入cdn链接,webpack系列-externals配置使用(CDN方式引入JS)

    • 方式一:使用html-webpack-externals-plugin
    • 方式二:直接配置externals

    如果需要引用一个库,但是又不想让webpack打包(减少打包的时间),并且又不影响我们在程序中以CMD、AMD或者window/global全局等方式进行使用(一般都以import方式引用使用),那就可以通过配置externals。

    这样做的目的就是将不怎么需要更新的第三方库脱离webpack打包,不被打入bundle中,从而减少打包时间,但又不影响运用第三方库的方式,例如import方式等。

     方式一:使用html-webpack-externals-plugin

    首先npm 安装html-webpack-externals-plugin,如下代码:

    npm i html-webpack-externals-plugin -D

    在我们常用的webpack.base.conf.js中的进行配置,我们以CDN引入vue框架为例,让其不打包到vendor.js中,在webpack.base.conf.js的配置如下:

    const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');
    
    module.exports = {
        // 其它省略...
        plugins: [
            new HtmlWebpackExternalsPlugin({
              externals: [{
                module: 'vue',
                entry: 'https://lib.baomitu.com/vue/2.6.12/vue.min.js',
                global: 'Vue'
              }]
            })
        ],
        // 其它省略...
    }

    最后看到在index.html中动态添加了如下代码:

     方式二:直接配置externals

    首先在index.html中script标签引入JS,如下代码:

    在webpack.base.conf.js的配置如下:

    module.exports = {
        // 其它省略...
        externals: {
            vue: 'Vue'
        },
        // 其它省略...
    }

     参考地址:

    • webpack 的externals配置
    • webpack学习笔记—plugins(html-webpack-externals-plugin)

    五十八、js判断数据类型的几种方法

    1. typeof

    鉴于 ECMAScript 是松散类型的,因此需要有种手段来检测给定变量的数据类型,typeof 就是负责提供这方面信息的操作符。对一个值使用 typeof 操作符可能返回下列某个字符串:( 缺点:对于数组和对象或null 都会返回object)

     "undefined"    ——如果这个值未定义;
    
      "boolean"    ——如果这个值是布尔值;
    
      "string"     ——如果这个值是字符串;
    
      "number"         ——如果这个值是数值;
    
      "object"   ——如果这个值是对象或 null;
    
      "function"  ——如果这个值是函数。
    
    
    
      例如:
    
      var message = "some string";
    
        alert(typeof message);   //"string"
    
        alert(typeof(message));    //"string"
    
        alert(typeof 95);             //"number"  
    
        var y=true;
    
        alert(typeof y);      //"boolean"
    
        var a = function() { };   
    
      alert(typeof a);     //"function"
    
      var b = [1,2,3];
    
        alert(typeof b);        //"object"
    
      var c = { }; 
    
      alert(typeof c);     //"object"
    
      var d = null;
    
      alert(typeof d);       //"object"

    2. 类型判断

      类型判断,一般就是判断是否是数组,是否是空对象。    

       (1) 判断是否是数组 

      定义一个数组:var a = [1,2,3,4,5];

      方法一:

      toString.call(a); // "[object Array]" 

      方法二:

      a instanceof Array; //true 

      方法三:

      a.constructor == Array; //true

      第一种方法比较通用,也就是Object.prototype.toString.call(a)的简写。

      instanceof和constructor判断的变量,必须在当前页面声明的,比如,一个页面(父页面)有一个框架,框架中引用了一个页面(子页面),在子页面中声明了一个a,并将其赋值给父页面的一个变量,这时判断该变量,Array == object.constructor会返回false;

      (2)判断是否是空对象

         定义一个变量:var obj = {};

      方法一:

      JSON.stringify(obj); // "{}"通过转换成JSON对象来判断是否是空大括号

      方法二:

      if(obj.id){ //如果属性id存在....}这个方法比较土,大多数人都能想到,前提是得知道对象中有某个属性。

      方法三: 

      function isEmptyObject(e) {

        var t; for (t in e) return !1; return !0 } //true isEmptyObject(obj);

        //false isEmptyObject({ "a":1, "b":2});

     

      这个方法是jQuery的isEmptyObject()方法的实现方式。

    五十九、Html5相对于Html4的新增的主体结构元素

    1.article元素

      这个元素表示文档,页面,应用程序或者站点中的自包含成分所构成的一个页面的一部分,并且这部分专门用于独立地分类或者复用,一个博客帖子,一个新的故事,视频或者是脚本,都很好的复合这一定义.除了内容部分,一个article元素通常有他自己的标题(通常放在一个header元素里面)有时可以做脚注.


         

        

    编程词典简介


    发表日期:

     

    编程词典加联加信息科技有限公司........



        

    版权所有,仿冒必究


     
      

    上面这个例子中,在header元素中嵌入文章的标题,其中标题潜在h1元素中,文章的发表日期嵌入在p元素中,在结尾处的footer元素中嵌入了文章的著作权,作为注脚,整个实例是比较完整的内容相对是独立的,因此可以使用article元素.另外article元素是可以嵌套使用的,内层的内容在原则上需要和外层的内容相关联.

     


         

        

    编程词典简介


    发表日期:

     

    编程词典加联加信息科技有限公司........



         

    评论


     

        

        

    发布者:小米




    加联加信息科技有限公司......


     

     

     
     
        

    发表者:zzj_zyy



    hahahahahhaha


     

     



        

    版权所有,仿冒必究


     
      

    具体来说实例内容分为几部分,文章标题放在了header元素中,文章正文放在了header元素后面的p元素里,然后section元素把正文与评论进行了区分,在section元素中嵌入了评论的内容,评论中每一个人的评论相对来说又是独立的完整的,因此对于他们都使用了一个article元素

    2.section元素

       section元素代表文档或应用程序中一般性的段或者节,段在这里是上下文种,指的是对内容按照主题的分组通常还附带标题,例如书的章节,带标签页的对话框的每个段落,但section元素并非一个普通的容器元素,作用是对页面上的内容进行分块,或者说对文章进行分段,但是不要和article混淆,因为article有着自己的完整的独立的内容.

       article和section的区别是社么呢?在html中,article元素可以看成是一种特殊种类的section元素,它比section元素更强调的是独立性,即section元素强调的是分段或者是分块,而article强调的是独立性,总结来说,如果一块内容比较独立就用article元素,但是如果一个内容分成几段,应该使用section元素;还要就是不用将section元素用作设置样式的页面容器,那是div的事,当arcicle元素,aside元素或者nav更符合要求时,尽量不用section;不要为没有标题的内容块使用section元素.

    3.nav元素

       nav元素用来构建导航,导航定义为一个页面中或者一个站点内的链接.nav元素的内容可以是链接的列表,标记为一个无序的列表或者是有序的列表,这里需要注意的是nav元素是一个包装器,它不会代替ol或者是ul元素的!!!!但是它会包围他.


        



     

      需要注意的是:在html中呢不要用menu元素来代替nav元素,因为menu元素是一系列发出命令的菜单上的,是一种交互性的元素或者确切的说是在Web应用程序中使用.

    4.aside元素

       aside元素用来表示当前页面或者文章的附属部分,也可以认为是

    5.time元素

    6.pubdate元素

    7.HTML5相对于html4 而言,新增了很多新的特性。增添了一些语义化标签,还有新增的一些表单类型和属性。今天就着重罗列一下表单新增的这些类型。

    新增input类型

    color 用户可以从中任意选取一个颜色

    date 可以从中选取一个日期

    datetime-local 从中选取日期和时间

    email 用于应该包含 e-mail 地址的输入域

    month 允许选择一个月份。

    number 用于应该包含数值的输入域。
    max 和min 可以限定数字的大小

    range 类型用于应该包含一定范围内数字值的输入域。
    显示为滑动条

    search 用于搜索域,比如站点搜索

    tel 定义输入电话号码

    time 选择一个时间

    url 定义输入网址

    week 选择周和年

    file 用于上传文件

    image 图片按钮 具有提交功能

    以下是最终效果图:

    å¨è¿éæå¥å¾çæè¿°

    六十、移动端 1像素问题及解决办法

    前言:在移动端web开发中,UI设计稿中设置边框为1像素,前端在开发过程中如果出现border:1px,测试会发现在某些机型上,1px会比较粗,即是较经典的 移动端1px像素问题。

    一、为什么会有1px问题
    要处理这个问题,必须先补充一个知识点,就是设备的 物理像素[设备像素] & 逻辑像素[CSS像素];

    物理像素:
    移动设备出厂时,不同设备自带的不同像素,也称硬件像素;
    逻辑像素:
    即css中记录的像素。

    在开发中,为什么移动端CSS里面写了1px,实际上看起来比1px粗;了解设备物理像素和逻辑像素的同学应该很容易理解,其实这两个px的含义其实是不一样的,UI设计师要求的1px是指设备的物理像素1px,而CSS里记录的像素是逻辑像素,它们之间存在一个比例关系,通常可以用
    javascript 中的 window.devicePixelRatio 来获取,也可以用媒体查询的
    -webkit-min-device-pixel-ratio 来获取。当然,比例多少与设备相关。 在手机上border无法达到我们想要的效果。这是因为 devicePixelRatio 特性导致,iPhone的
    devicePixelRatio==2,而 border-width: 1px;
    描述的是设备独立像素,所以,border被放大到物理像素2px显示,在iPhone上就显得较粗。
    二、解决方案
    1、 媒体查询利用设备像素比缩放,设置小数像素;
    优点:简单,好理解
    缺点:兼容性差,目前之余IOS8+才支持,在IOS7及其以下、安卓系统都是显示0px。

    IOS8+下已经支持带小数的px值,media query 对应 devicePixelRatio 有个查询值 -webkit-min-device-pixel-ratio;

    1.)css可以写成这样:

    @media screen and (-webkit-min-device-pixel-ratio: 2) {
        .border { border: 0.5px solid #999 }
    }
    @media screen and (-webkit-min-device-pixel-ratio: 3) {
        .border { border: 0.333333px solid #999 }
    }

    2.)js 可以写成这样:

    二者任选其一;
    2、设置 border-image 方案
    缺点:需要制作图片,圆角可能出现模糊

    .border-image-1px {
        border-width: 1px 0px;
        -webkit-border-image: url("border.png") 2 0 stretch;
        border-image: url("border.png") 2 0 stretch;
    }

    border-width:指定边框的宽度,可以设定四个值,分别为上右下左border-width: top right bottom left。
    border-image:该例意为:距离图片上方2px(属性值上没有单位)裁剪边框图片作为上边框,下方2px裁剪作为下边框。距离左右0像素裁剪图片即没有边框,以拉伸方式展示。
    3、background-image 渐变实现
    除了使用图片外,当然也能使用纯css来实现,百度糯米团就是采用的这种方案。
    缺点:因为每个边框都是线性渐变颜色实现,因此无法实现圆角。

    .border {
          background-image:linear-gradient(180deg, red, red 50%, transparent 50%),
          linear-gradient(270deg, red, red 50%, transparent 50%),
          linear-gradient(0deg, red, red 50%, transparent 50%),
          linear-gradient(90deg, red, red 50%, transparent 50%);
          background-size: 100% 1px,1px 100% ,100% 1px, 1px 100%;
          background-repeat: no-repeat;
          background-position: top, right top,  bottom, left top;
          padding: 10px;
    }

    原理:将原本1个物理像素的边框大小利用线性渐变分割成几个部分(百分比控制),实现小于1像素效果。
    linear-gradient:指定线性渐变,接受大于等于3个参数,第一个为渐变旋转角度,第二个开始为渐变的颜色和到哪个位置(百分比)全部变为该颜色,该例子中,第一句就是,渐变方向旋转180度,即从上往下(默认为0度从下往上),从红色开始渐变,到50%的位置还是红色,再渐变为继承父元素颜色。
    4、box-shadow
    利用阴影也可以实现,优点是没有圆角问题,缺点是颜色不好控制

    div {
        -webkit-box-shadow: 0 1px 1px -1px rgba(0, 0, 0, 0.5);
    }

    box-shadow属性的用法:box-shadow: h-shadow v-shadow [blur] [spread] [color] [inset]
    参数分别表示: 水平阴影位置,垂直阴影位置,模糊距离, 阴影尺寸,阴影颜色,将外部阴影改为内部阴影,后四个可选。该例中为何将阴影尺寸设置为负数?设置成-1px 是为了让阴影尺寸稍小于div元素尺寸,这样左右两边的阴影就不会暴露出来,实现只有底部一边有阴影的效果。从而实现分割线效果(单边边框)。
    5、viewport + rem
    该方案是对上述方案的优化,整体思路就是利用viewport + rem + js 动态的修改页面的缩放比例,实现小于1像素的显示。
    缺点:以为缩放涉及全局的rem单位,比较适合新项目,对于老项目可能要涉及到比较多的改动。

    在页面初始化时,在头部引入原始默认状态如下:

    
    

    接下来的任务就是js的动态修改缩放比 以及 实现rem根元素字体大小的设置。

    var viewport = document.querySelector("meta[name=viewport]")
    if (window.devicePixelRatio == 1) {
        viewport.setAttribute('content', 'width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no')
    } 
    if (window.devicePixelRatio == 2) {
        viewport.setAttribute('content', 'width=device-width, initial-scale=0.5, maximum-scale=0.5, minimum-scale=0.5, user-scalable=no')
    } 
    if (window.devicePixelRatio == 3) {
        viewport.setAttribute('content', 'width=device-width, initial-scale=0.333333333, maximum-scale=0.333333333, minimum-scale=0.333333333, user-scalable=no')
    } 
    var docEl = document.documentElement;
    var fontsize = 10 * (docEl.clientWidth / 320) + 'px';
    docEl.style.fontSize = fontsize;


    6、transform: scale(0.5) 方案 - 推荐: 很灵活
    1.) 设置height: 1px,根据媒体查询结合transform缩放为相应尺寸。

    div {
        height:1px;
        background:#000;
        -webkit-transform: scaleY(0.5);
        -webkit-transform-origin:0 0;
        overflow: hidden;
    }

    2.) 用::after和::befor,设置border-bottom:1px solid #000,然后在缩放-webkit-transform: scaleY(0.5);可以实现两根边线的需求

    div::after{
        content:'';width:100%;
        border-bottom:1px solid #000;
        transform: scaleY(0.5);
    }
    

    3.) 用::after设置border:1px solid #000; width:200%; height:200%,然后再缩放scaleY(0.5); 优点可以实现圆角,京东就是这么实现的,缺点是按钮添加active比较麻烦。

    .div::after {
        content: '';
        width: 200%;
        height: 200%;
        position: absolute;
        top: 0;
        left: 0;
        border: 1px solid #bfbfbf;
        border-radius: 4px;
        -webkit-transform: scale(0.5,0.5);
        transform: scale(0.5,0.5);
        -webkit-transform-origin: top left;
    }


    7、媒体查询 + transfrom 对方案1的优化

    /* 2倍屏 */
    @media only screen and (-webkit-min-device-pixel-ratio: 2.0) {
        .border-bottom::after {
            -webkit-transform: scaleY(0.5);
            transform: scaleY(0.5);
        }
    }
    /* 3倍屏 */
    @media only screen and (-webkit-min-device-pixel-ratio: 3.0) {
        .border-bottom::after {
            -webkit-transform: scaleY(0.33);
            transform: scaleY(0.33);
        }
    }

    六十一、微信小程序面试题

    1.  小程序有几个文件?

    • WXML:微信自己定义的一套组件

    • WXSS :    用于描述 WXML 的组件样式

    • js :   逻辑处理

    • json  :  小程序页面配置

    • WXS

      WXS(WeiXin Script)是小程序的一套脚本语言,结合 WXML,可以构建出页面的结构

    2.小程序怎么跟随事件传值

    在  页面标签上通过 绑定 dataset-key = value , 然后绑定点击通过e.currentTarget.dataset.key  来获取标签上绑定的值。

     拿到传值
    
    get(e){
        console.log(e.currentTarget.dataset.name)
      },

    3. 小程序 WXSS  与 CSS 的区别

    WXSS

    • wxss 背景图片只能引入外链,不能使用本地图片

    • 小程序样式使用 @import 引入 外联样式文件,地址为相对路径。

    • 尺寸单位为  rpx , rpx 是响应式像素,可以根据屏幕宽度进行自适应。

    4. 小程序的双向绑定和Vue哪里不一样。

    小程序 直接使用this.data.key = value  是  不能更新到视图当中的。

    必须使用  this.setData({ key :value })  来更新值。

    5. 小程序的生命周期函数

    • onLoad  :  页面加载时触发。一个页面只会调用一次,可以在 onLoad的参数中获取打开当前页面路径中的参数

    • onShow :   页面显示 / 切入前台时触发调用。

    • onReady :  页面初次渲染完成时触发,一个页面只会调用一次。

    • onHide : 页面隐藏 / 切入后台时触发,如 navigateTo 或底部 tab切换到其他页面,小程序切入后台等

    • onUnload : 页面卸载时触发。如 redirectTo或 navigateBack 到其他页面时.

    6. 小程序怎么实现下拉刷新

    两种方案

    方案 一 :

    • 通过在 app.json  中, 将 "enablePullDownRefresh": true,    开启全局下拉刷新。

    • 或者通过在 组件 .json ,  将 "enablePullDownRefresh": true,    单组件下拉刷新。

    方案二:

    • scroll-view  :使用该滚动组件 自定义刷新,通过 bindscrolltoupper  属性, 当滚动到顶部/左边,会触发 scrolltoupper事件,所以我们可以利用这个属性,来实现下拉刷新功能。

    7.  bindtap  和  catchtap 区别

    相同点: 都是点击事件

    不同点: bindtap 不会阻止冒泡,   catchtap 可以阻止冒泡。

    8. 小程序有哪些传递数据的方法

    1. 使用全局变量

    • 在 app.js  中的   this.globalData = { }   中放入要存储的数据。

    • 在 组件.js 中, 头部 引入   const app = getApp(); 获取到全局变量

    • 直接使用 app.globalData.key  来进行赋值和获取值。

    2. 使用 路由

    • wx.navigateTo    和 wx.redirectTo 时,可以通过在 url  后 拼接 + 变量, 然后在 目标页面  通过在   onLoad 周期中,通过参数来获取传递过来的值。

    3. 使用本地缓存

    9. 简述下 wx.navigateTo()wx.redirectTo()wx.switchTab()wx.navigateBack()wx.reLaunch()区别

    • wx.navigateTo() : 保留当前页面,跳转到应用内的某个页面。但是不能跳到 tabbar 页面

    • wx.redirectTo() :  关闭当前页面,跳转到应用内的某个页面。但是不允许跳转到 tabbar 页面

    • wx.switchTab()  :  跳转到 TabBar 页面,并关闭其他所有非 tabBar 页面

    • wx.navigateBack() : 关闭当前页面,返回上一页面或多级页面。可通过 getCurrentPages() 获取当前的页面栈,决定需要返回几层

    • wx.reLaunch() :  关闭所有页面,打开到应用的某个页面。

    10.  小程序 wx:if   和  hidden 的区别

    • wx:if :  有更高的切换消耗。

    • hidden : 有更高的初始渲染消耗。

    使用

    • 频繁切换使用 hidden,   运行时条件变化使用 wx: if

    11. app.json  全局配置文件描述

    • pages  :  用于存放当前小程序的所有页面路径

    • window : 小程序所有页面的顶部背景颜色,文字颜色配置。

    • tabBar  :  小程序底部的 Tab ,最多5个,最少2个。

    12. 如何封装小程序请求

      1. 封装 wx.request  请求传递需要的参数( url ,   data ,   method , success 成功回调    , fail 失败回调 )  , 封装常用方法 POST ,   GET  , DELETE , PUT  ....  最后导出这些方法

    •  

      1. 然后新建一个 api.js  文件,导入封装好的方法,然后调取相应的方法,传递数据。

    wx.request 封装

    var app = getApp(); //获取小程序全局唯一app实例
    var host = '******************'; //接口地址
     
    
     
    //POST请求
    function post(url, data, success,fail) {
      request(url, postData, "POST", doSuccess, doFail);
    }
     
    //GET请求
    function get(url, data, success, fail) {
      request(url, postData, "GET", doSuccess, doFail);
    }
     
    function request(url, data, method, success, fail) {
      wx.showLoading({
        title: "正在加载中...",
      })
      wx.request({
        url: host + url, //请求地址
        method: method, //请求方法
        header: { //请求头
          "Content-Type": "application/json;charset=UTF-8"
        },
        data: data, //请求参数    
        dataType: 'json', //返回数据格式
        responseType: 'text', //响应的数据类型
        success: function(res) {
          wx.hideLoading();
          //成功执行方法,参数值为res.data,直接将返回的数据传入
          success(res.data);
        },
        fail: function() {
          //失败执行方法
          fail();
        },
      })
    }
    module.exports = {
      postRequest: post,
      getRequest: get,
    }
    

    组件使用 封装好的请求

    var http = require('../../utils/request.js'); //相对路径
    
    
    var params = {//请求参数
      id:this.data.userId
    }
    http.postRequest("user/delUser", params, function(res) {
      console.log("修改成功!");
      
    }, function(res) {
      console.log("修改失败!!!")
    })

    13. 小程序运行机制

    • 热启动 :假如用户已经打开了某个小程序,在一定时间内再次打开小程序的话,这个时候我们就不再需要重新启动了,这需要把我们的后台打开的小程序切换到前台来使用。

    • 冷启动:用户首次打开小程序或被微信主动销毁再次打开的情况,此时小程序需要重新加载启动。

    14. 小程序什么时候会主动销毁?

    小程序在进入后台之后,客户端会帮我们在一定时间内维持我们的一个状态,超过五分钟后,会被微信主动销毁.

    官方也没有明确说明 什么时候销毁, 在不同机型表现也不一样,

    2019年开发时:时间官方文档没有说明,但是经过询问一般指5分钟内

    2020年开发时:时间官方文档没有说明,实测安卓没有固定时间,内存足够情况下,有时候一天了还在,有时候几分钟就没了。

    15. 微信授权流程

    前端常问_第36张图片

    六十二、vue的v-model原理简述

    一,v-model是什么

    v-model就是vue的双向绑定的指令,能将页面上控件输入的值同步更新到相关绑定的data属性,也会在更新data绑定属性时候,更新页面上输入控件的值。

    二,为什么使用v-model

    v-model作为双向绑定指令也是vue两大核心功能之一,使用非常方便,提高前端开发效率。在view层,model层相互需要数据交互,即可使用v-model。

    三,v-model的原理简单描述

    v-model主要提供了两个功能,view层输入值影响data的属性值,data属性值发生改变会更新view层的数值变化。以下以input控制绑定v-model举例说明。

     

    
    
    
       
    
    
       

    {{name}}

    3.1 input 输入值后更新data

      首先在页面初始化时候,vue的编译器会编译该html模板文件,将页面上的dom元素遍历生成一个虚拟的dom树。再递归遍历虚拟的dom的每一个节点。当匹配到其是一个元素而非纯文本,则继续遍历每一个属性。
      如果遍历到v-model这个属性,则会为这个节点添加一个input事件,当监听从页面输入值的时候,来更新vue实例中的data想对应的属性值。

     

        // 假如node是遍历到的input节点
        node.addEventListener("input",function(e){
            vm.name=e.target.value;
        })  
    

    3.2data的属性赋值后更新input的值

      同样初始化vue实例时候,会递归遍历data的每一个属性,并且通过defineProperty来监听每一个属性的get,set方法,从而一旦某个属性重新赋值,则能监听到变化来操作相应的页面控制。

     

        Object.defineProperty(data,"name",{
            get(){
                return data["name"];
            },
            set(newVal){
                let val=data["name"];
                if (val===newVal){
                    return;
                }
                data["name"]=newVal;
                // 监听到了属性值的变化,假如node是其对应的input节点
                node.value=newVal;
            }    
        })
    

    四,总结

      其核心就是,一方面modal层通过defineProperty来劫持每个属性,一旦监听到变化通过相关的页面元素更新。另一方面通过编译模板文件,为控件的v-model绑定input事件,从而页面输入能实时更新相关data属性值。

    六十三、http状态码301和302的区别?304是干什么的?

    301、302、304
    301 Moved Permanently 永久移动。是指请求的资源已被永久的移动到新的URL,返回信息会包括新的URL,浏览器还会自动定向到新的URL。今后任何新的请求都应该使用新的URL来代替。
    302 Found 临时移动。与301类似。但是资源只是临时被移动。客户端应该继续使用原有的URI
    304 Not Modified 未修改。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。客户端通常会缓存所访问过的资源。通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源。
    什么时候会出现304
    如果客户端发送了一个带条件的GET请求已被允许,而文档中的内容(自上次访问以来或者根据请求的条件)并没有改变,则服务器应当返回这个304状态码。即客户端和服务器端只需要传输很少的数据量来做文件的校验,如果文件没有修改过,则不需要返回全量的数据。

    304的缓存原理
    1.服务器首先产生Etag,服务器可在稍后使用它来判断页面是否被修改。本质上客户端通过该记号传回服务器要求服务器验证(客户端)缓存。
    2.304是HTTP的状态码,服务器用来标识这个文件没有被修改,不返回内容,浏览器接受到这个状态码会去去找浏览器缓存的文件。
    3.流程:

    客户端请求一个页面A。服务器返回页面A,并在A上加一个Tage客服端渲染该页面,并把Tage也存储在缓存中。客户端再次请求页面A。
    并将上次请求的资源和ETage一起传递给服务器。服务器检查Tage.并且判断出该页面自上次客户端请求之后未被修改。直接返回304。
    last-modified: 客服端请求资源,同时有一个last-modified的属性标记此文件在服务器最后修改的时间
    客服端第二次请求此url时,根据http协议。浏览器会向服务器发送一个If-Modified-Since报头,
    询问该事件之后文件是否被修改,没修改返回304。
    有了Last-Modified,为什么还要用ETag?
    1、因为如果在一秒钟之内对一个文件进行两次更改,Last-Modified就会不正确(Last—Modified不能识别秒单位的修改)
    2、某些服务器不能精确的得到文件的最后修改时间
    3、一些文件也行会周期新的更改,但是他的内容并不改变(仅仅改变修改的事件),这个时候我们并不希望客户端认为文件被修改,而重新Get。

     ETag,为什么还要用Last-Modified?
     1、两者互补,ETag的判断的缺陷,比如一些图片等静态文件的修改
     2、如果每次扫描内容都生成ETag比较,显然要比直接比较修改时间慢的多。
    301和302的区别
    (1)字面上区别:301永久重定向 302临时重定向。
    (2)301比较常用的场景是使用域名跳转。302用来做临时跳转 (例如:未登录的用户访问用户中心重定向到登录页面)

    六十四、url回车之后的发生的事情?

    前言

      这个问题已经是老生常谈了,更是经常被作为面试的压轴题出现,网上也有很多文章,但最近闲的无聊,然后就自己做了一篇笔记,感觉比之前理解更透彻了。

      这篇笔记是我这两天看了数十篇文章总结出来的,所以相对全面一点,但由于我是做前端的,所以会比较重点分析浏览器渲染页面那一部分,至于其他部分我会罗列出关键词,感兴趣的可以自行查阅,

     

    注意:本文的步骤是建立在,请求的是一个简单的 HTTP 请求,没有 HTTPS、HTTP2、最简单的 DNS、没有代理、并且服务器没有任何问题的基础上,尽管这是不切实际的。

     

    大致流程

    1. URL 解析

    2. DNS 查询

    3. TCP 连接

    4. 处理请求

    5. 接受响应

    6. 渲染页面

     

    一、URL 解析

      地址解析:

        首先判断你输入的是一个合法的 URL 还是一个待搜索的关键词,并且根据你输入的内容进行自动完成、字符编码等操作。

      HSTS

        由于安全隐患,会使用 HSTS 强制客户端使用 HTTPS 访问页面。详见:你所不知道的 HSTS[1]。

      其他操作

        浏览器还会进行一些额外的操作,比如安全检查、访问限制(之前国产浏览器限制 996.icu)。

      检查缓存

     

     

        

     

    二、DNS 查询

      基本步骤

        

     

      1. 浏览器缓存

        浏览器会先检查是否在缓存中,没有则调用系统库函数进行查询。

      2. 操作系统缓存

        操作系统也有自己的 DNS缓存,但在这之前,会向检查域名是否存在本地的 Hosts 文件里,没有则向 DNS 服务器发送查询请求。

      3. 路由器缓存

        路由器也有自己的缓存。

      4. ISP DNS 缓存

        ISP DNS 就是在客户端电脑上设置的首选 DNS 服务器,它们在大多数情况下都会有缓存。

      根域名服务器查询

        在前面所有步骤没有缓存的情况下,本地 DNS 服务器会将请求转发到互联网上的根域,下面这个图很好的诠释了整个流程:

     

    需要注意的点

    1. 递归方式:一路查下去中间不返回,得到最终结果才返回信息(浏览器到本地DNS服务器的过程)

    2. 迭代方式,就是本地DNS服务器到根域名服务器查询的方式。

    3. 什么是 DNS 劫持

    4. 前端 dns-prefetch 优化

     

     

    三、TCP 连接

      TCP/IP 分为四层,在发送数据时,每层都要对数据进行封装:

        

     

    1. 应用层:发送 HTTP 请求

      在前面的步骤我们已经得到服务器的 IP 地址,浏览器会开始构造一个 HTTP 报文,其中包括:

      • 请求报头(Request Header):请求方法、目标地址、遵循的协议等等

      • 请求主体(其他参数)

      其中需要注意的点:

      • 浏览器只能发送 GET、POST 方法,而打开网页使用的是 GET 方法

    2. 传输层:TCP 传输报文

      传输层会发起一条到达服务器的 TCP 连接,为了方便传输,会对数据进行分割(以报文段为单位),并标记编号,方便服务器接受时能够准确地还原报文信息。

      在建立连接前,会先进行 TCP 三次握手。

    关于 TCP/IP 三次握手,网上已经有很多段子和图片生动地描述了。

    相关知识点:

    1. SYN 泛洪攻击

    3. 网络层:IP协议查询Mac地址

      将数据段打包,并加入源及目标的IP地址,并且负责寻找传输路线。

      判断目标地址是否与当前地址处于同一网络中,是的话直接根据 Mac 地址发送,否则使用路由表查找下一跳地址,以及使用 ARP 协议查询它的 Mac 地址。

    注意:在 OSI 参考模型中 ARP 协议位于链路层,但在 TCP/IP 中,它位于网络层。

    4. 链路层:以太网协议

      以太网协议

        根据以太网协议将数据分为以“帧”为单位的数据包,每一帧分为两个部分:

        • 标头:数据包的发送者、接受者、数据类型

        • 数据:数据包具体内容

      Mac 地址

        以太网规定了连入网络的所有设备都必须具备“网卡”接口,数据包都是从一块网卡传递到另一块网卡,网卡的地址就是 Mac 地址。每一个 Mac 地址都是独一无二的,具备了一对一的能力。

      广播

        发送数据的方法很原始,直接把数据通过 ARP 协议,向本网络的所有机器发送,接收方根据标头信息与自身 Mac 地址比较,一致就接受,否则丢弃。

      注意:接收方回应是单播。

    相关知识点:

    1. ARP 攻击

      服务器接受请求

        接受过程就是把以上步骤逆转过来,参见上图。

     

    四、服务器处理请求

      大致流程

        

     

     

     

      HTTPD

        最常见的 HTTPD 有 Linux 上常用的 Apache 和 Nginx,以及 Windows 上的 IIS。

        它会监听得到的请求,然后开启一个子进程去处理这个请求。

      处理请求

        接受 TCP 报文后,会对连接进行处理,对HTTP协议进行解析(请求方法、域名、路径等),并且进行一些验证:

        • 验证是否配置虚拟主机

        • 验证虚拟主机是否接受此方法

        • 验证该用户可以使用该方法(根据 IP 地址、身份信息等)

      重定向

        假如服务器配置了 HTTP 重定向,就会返回一个 301永久重定向响应,浏览器就会根据响应,重新发送 HTTP 请求(重新执行上面的过程)。

    关于更多:详见这篇文章[2]

      URL 重写

        然后会查看 URL 重写规则,如果请求的文件是真实存在的,比如图片、html、css、js文件等,则会直接把这个文件返回。

        否则服务器会按照规则把请求重写到 一个 REST 风格的 URL 上。

        然后根据动态语言的脚本,来决定调用什么类型的动态文件解释器来处理这个请求。

        以 PHP 语言的 MVC 框架举例,它首先会初始化一些环境的参数,根据 URL 由上到下地去匹配路由,然后让路由所定义的方法去处理请求。

     

    五、浏览器接受响应

      浏览器接收到来自服务器的响应资源后,会对资源进行分析。

      首先查看 Response header,根据不同状态码做不同的事(比如上面提到的重定向)。

      如果响应资源进行了压缩(比如 gzip),还需要进行解压。

      然后,对响应资源做缓存。

      接下来,根据响应资源里的 MIME[3] 类型去解析响应内容(比如 HTML、Image各有不同的解析方式)。

    六、渲染页面

      浏览器内核

     

        

     

      不同的浏览器内核,渲染过程也不完全相同,但大致流程都差不多。

    基本流程

     

         

     

     

     

    6.1. HTML 解析

        首先要知道浏览器解析是从上往下一行一行地解析的。

        解析的过程可以分为四个步骤:

      ① 解码(encoding)

        传输回来的其实都是一些二进制字节数据,浏览器需要根据文件指定编码(例如UTF-8)转换成字符串,也就是HTML 代码。

      ② 预解析(pre-parsing)

        预解析做的事情是提前加载资源,减少处理时间,它会识别一些会请求资源的属性,比如img标签的src属性,并将这个请求加到请求队列中。

      ③ 符号化(Tokenization)

        符号化是词法分析的过程,将输入解析成符号,HTML 符号包括,开始标签、结束标签、属性名和属性值。

        它通过一个状态机去识别符号的状态,比如遇到<>状态都会产生变化。

      ④ 构建树(tree construction)

    注意:符号化和构建树是并行操作的,也就是说只要解析到一个开始标签,就会创建一个 DOM 节点。

      在上一步符号化中,解析器获得这些标记,然后以合适的方法创建DOM对象并把这些符号插入到DOM对象中。

    复制代码

    
    
        Web page parsing
    
    
        

    Web page parsing

    This is an example Web page.

    复制代码

     

     

     

     

      浏览器容错进制

        你从来没有在浏览器看过类似”语法无效”的错误,这是因为浏览器去纠正错误的语法,然后继续工作。

      事件

        当整个解析的过程完成以后,浏览器会通过DOMContentLoaded事件来通知DOM解析完成。

      6.2. CSS 解析

        一旦浏览器下载了 CSS,CSS 解析器就会处理它遇到的任何 CSS,根据语法规范[4]解析出所有的 CSS 并进行标记化,然后我们得到一个规则表。

      CSS 匹配规则

        在匹配一个节点对应的 CSS 规则时,是按照从右到左的顺序的,例如:div p { font-size :14px }会先寻找所有的p标签然后判断它的父元素是否为div

        所以我们写 CSS 时,尽量用 id 和 class,千万不要过度层叠。

      6.3. 渲染树

        其实这就是一个 DOM 树和 CSS 规则树合并的过程。

    注意:渲染树会忽略那些不需要渲染的节点,比如设置了display:none的节点。

      计算

        通过计算让任何尺寸值都减少到三个可能之一:auto、百分比、px,比如把rem转化为px

      级联

        浏览器需要一种方法来确定哪些样式才真正需要应用到对应元素,所以它使用一个叫做specificity的公式,这个公式会通过:

      1. 标签名、class、id

      2. 是否内联样式

      3. !important

      然后得出一个权重值,取最高的那个。

      渲染阻塞

        当遇到一个script标签时,DOM 构建会被暂停,直至脚本完成执行,然后继续构建 DOM 树。

        但如果 JS 依赖 CSS 样式,而它还没有被下载和构建时,浏览器就会延迟脚本执行,直至 CSS Rules 被构建。

      所有我们知道:

      • CSS 会阻塞 JS 执行

      • JS 会阻塞后面的 DOM 解析

      为了避免这种情况,应该以下原则:

      • CSS 资源排在 JavaScript 资源前面

      • JS 放在 HTML 最底部,也就是 

      另外,如果要改变阻塞模式,可以使用 defer 与 async,详见:这篇文章[5]

      6.4. 布局与绘制

        确定渲染树种所有节点的几何属性,比如:位置、大小等等,最后输入一个盒子模型,它能精准地捕获到每个元素在屏幕内的准确位置与大小。

        然后遍历渲染树,调用渲染器的 paint() 方法在屏幕上显示其内容。

      6.5. 合并渲染层

        把以上绘制的所有图片合并,最终输出一张图片。

      6.6. 回流与重绘

      回流(reflow)

        当浏览器发现某个部分发现变化影响了布局时,需要倒回去重新渲染,会从html标签开始递归往下,重新计算位置和大小。

        reflow基本是无法避免的,因为当你滑动一下鼠标、resize 窗口,页面就会产生变化。

      重绘(repaint)

        改变了某个元素的背景色、文字颜色等等不会影响周围元素的位置变化时,就会发生重绘。

        每次重绘后,浏览器还需要合并渲染层并输出到屏幕上。

        回流的成本要比重绘高很多,所以我们应该尽量避免产生回流。

      比如:

      • display:none 会触发回流,而 visibility:hidden 只会触发重绘。

      6.7. JavaScript 编译执行

      大致流程

        

     

     

     

     

      可以分为三个阶段:

      1. 词法分析

        JS 脚本加载完毕后,会首先进入语法分析阶段,它首先会分析代码块的语法是否正确,不正确则抛出“语法错误”,停止执行。

       几个步骤:

      • 分词,例如将var a = 2,,分成vara=2这样的词法单元。

      • 解析,将词法单元转换成抽象语法树(AST)。

      • 代码生成,将抽象语法树转换成机器指令。

      2. 预编译

        JS 有三种运行环境:

      • 全局环境

      • 函数环境

      • eval

        每进入一个不同的运行环境都会创建一个对应的执行上下文,根据不同的上下文环境,形成一个函数调用栈,栈底永远是全局执行上下文,栈顶则永远是当前执行上下文。

      创建执行上下文

      创建执行上下文的过程中,主要做了以下三件事:

    • 创建变量对象

      • 参数、函数、变量

    • 建立作用域链

      • 确认当前执行环境是否能访问变量

    • 确定 This 指向

      3. 执行

        JS 线程

     

     

     

      虽然 JS 是单线程的,但实际上参与工作的线程一共有四个:

    其中三个只是协助,只有 JS 引擎线程是真正执行的

    • JS 引擎线程:也叫 JS 内核,负责解析执行 JS 脚本程序的主线程,例如 V8 引擎

    • 事件触发线程:属于浏览器内核线程,主要用于控制事件,例如鼠标、键盘等,当事件被触发时,就会把事件的处理函数推进事件队列,等待 JS 引擎线程执行

    • 定时器触发线程:主要控制setIntervalsetTimeout,用来计时,计时完毕后,则把定时器的处理函数推进事件队列中,等待 JS 引擎线程。

    • HTTP 异步请求线程:通过XMLHttpRequest连接后,通过浏览器新开的一个线程,监控readyState状态变更时,如果设置了该状态的回调函数,则将该状态的处理函数推进事件队列中,等待JS引擎线程执行。

      注:浏览器对同一域名的并发连接数是有限的,通常为 6 个。

      宏任务

        分为:

      • 同步任务:按照顺序执行,只有前一个任务完成后,才能执行后一个任务

      • 异步任务:不直接执行,只有满足触发条件时,相关的线程将该异步任务推进任务队列中,等待JS引擎主线程上的任务执行完毕时才开始执行,例如异步Ajax、DOM事件,setTimeout等。

      微任务

        微任务是ES6和Node环境下的,主要 API 有:Promiseprocess.nextTick

        微任务的执行在宏任务的同步任务之后,在异步任务之前。

        

     

     

     

     

      代码例子

     

    复制代码

    console.log('1'); // 宏任务 同步
    
    setTimeout(function() {
        console.log('2'); // 宏任务 异步
    })
    
    new Promise(function(resolve) {
        console.log('3'); // 宏任务 同步
        resolve();
    }).then(function() {
        console.log('4') // 微任务
    })
    
    console.log('5') // 宏任务 同步

    复制代码

     

      以上代码输出顺序为:1,3,5,4,2

     

    参考文档

    [1]你所不知道的 HSTS: http://t.cn/AiR8pTqx
    [2]详见这篇文章: http://t.cn/AiR8pnEC
    [3]MIME: http://t.cn/AiR8prtm
    [4]语法规范: http://t.cn/AiR80GdO
    [5]这篇文章: http://t.cn/AiR80c1k
    [6]what-happens-when-zh_CN: http://t.cn/AiR80xb5http://t.cn/AiR80xb5http://t.cn/AiR80xb5
    [7]Tags to DOM:http://t.cn/AiR80djX
    [8]彻底理解浏览器的缓存机制: http://t.cn/AiR8Ovob
    [9]浏览器的工作原理:新式网络浏览器幕后揭秘: http://t.cn/AiR8Oz06
    [10]深入浅出浏览器渲染原理: http://t.cn/AiR8O4fO

    六十五、http和https的区别?

     http的全称是Hypertext Transfer Protocol Vertion (超文本传输协议)

    HTTPS的全称是Secure Hypertext Transfer Protocol(安全超文本传输协议),是在http协议基础上增加了使用SSL加密传送信息的协议。

        HTTPS和HTTP的区别:

          https协议需要到ca申请证书,一般免费证书很少,需要交费。

          http是超文本传输协议,信息是明文传输,https 则是具有安全性的ssl加密传输协议。

          http和https使用的是完全不同的连接方式用的端口也不一样,前者是80,后者是443。

          http的连接很简单,是无状态的。

          HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全。

          HTTPS解决的问题:

          1 . 信任主机的问题. 采用https 的server 必须从CA 申请一个用于证明服务器用途类型的证书. 改证书只有用于对应的server 的时候,客户度才信任次主机。所以目前所有的银行系统网站,关键部分应用都是https 的,客户通过信任该证书,从而信任了该主机,其实这样做效率很低,但是银行更侧重安全。这一点对我们没有任何意义,我们的server 采用的证书不管自己issue 还是从公众的地方issue,客户端都是自己人,所以我们也就肯定信任该server。
     
          2 . 通讯过程中的数据的泄密和被窜改

你可能感兴趣的