高级知识点(持续更新)

12,709 阅读17分钟

基础知识点地址(持续更新): juejin.cn/post/686307…

webpack

loader是什么?

Loader本质就是一个函数,在该函数中对接收到的内容进行转换,返回转换后的结果。因为Webpack只认识JavaScript,所以Loader就成了翻译官,对其他类型的资源进行转译预处理工作。

loader在module.rules中配置:test和use。test可接收一个正则表达式,只有正则匹配上的模块才使用这条规则;use可接收一个数组,数组包含该规则所使用的loader。

常见的loader:

  • file-loader 使得我们可以在JS文件中引入png\jpg等图片资源
  • url-loader 跟file-loader类似;唯一不同的是在于用户可以设置一个文件大小的阈值,当大于阈值时跟file-loader一样返回publicPath,而小于该阈值时则返回文件base64形式编码。
  • style-loader css-loader 其中css-loader处理js中import require() @import/url 引入的内容;style-loader 负责把样式插入到DOM中,方法是在head中插入一个style标签,并把样式写入到这个标签的 innerHTML里。
  • sass-loader 把scss转成css
  • less-loader 把less转成css
  • babel-loader 中间桥梁,通过调用babel/core中的api来告诉webpack要如何处理 js。
    • babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。
    • @babel/core是babel的核心库,所有的核心API都在这个库里,这些API供babel-loader调用
    • @babel/preset-env 最主要的配置字段是 useBuiltIns 和 target;target表示所要编译的代码运行环境,可以是浏览器,可以是node。只要设置相应的字段,他就能根据规则和转义相应的语法跟API。useBuiltIns:不需要手动 import '@babel/polyfill'; 业务代码使用到的新的API按需进行polyfill。
    "@babel/env",
      {
        targets: {
          "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 
          //表示兼容市场占有率>1%,浏览器的最新两个版本,ie8以下不兼容
        },
        useBuiltIns: "usage",
      },
    
    • @babel/preset-env只是提供了语法转换的规则,但是它并不能弥补浏览器缺失的一些新的功能。
    原始代码: 只是@babel/preset-env 配置了@babel/polyfill "useBuiltIns": "usage" 自定义loader segmentfault.com/a/119000001…

Plugin

Plugin 就是插件,基于事件流框架 Tapable,插件可以扩展Webpack的功能,在Webpack运行的生命周期中会广播出许多事件,Plugin可以监听这些事件,在合适的时机通过Webpack提供的API改变输出结果。

常用的plugin:

  • html-webpack-plugin 自动生成HTML5文件,并引入webpack打包好的 js 等文件。
  • clean-webpack-plugin 用于打包前先把dist文件夹清空
  • hot-module-replacement-plugin 模块热替换插件,即HMR,webpack4 自带插件,无需安装,在开发模式下配合 devServer 使用
  • mini-css-extract-plugin 将CSS提取到单独的文件中,类似功能的有 extract-text-webpack-plugin (webpack4 已废弃)。两者相比,mini-css-extract-plugin 的优点:
    • 异步加载
    • 没有重复的编译(性能)
    • 更容易使用
    • 特定CSS
  • PurgecssPlugin 可以去除未使用的 css, 一般与glob、glob-all 配合使用。
  • optimize-css-assets-webpack-plugin 用于 CSS 压缩
  • commons-chunk-plugin 用于提取 js 中公共代码(webpack4 已废弃)
    插一句:公共模块代码的提取好处:开发过程中减少了重复模块打包,可以提升开发速度;减小整体资源体积;合理分片后的代码可以更有效地利用客服端缓存。
  • split-chunks-plugin 用于提取 js 中公共代码。webpack4 内置插件。相比于 commons-chunk-plugin 的优点:
    • 从命令式到声明式
    • 优化了 commons-chunk-plugin 在异步提取公共模块代码的问题(不能正确提取异步代码的公共模块)。
  • webpack-bundle-analyzer 可视化 webpack 输出文件的体积 mp.weixin.qq.com/s/WfW_L0Qs1… www.cnblogs.com/guolao/arch… blog.csdn.net/weixin_4390… www.cnblogs.com/susouth/p/1…

webpack热更新原理

webpack本身命令行并不支持HMR,我们可以使用 webpack-dev-server 配合 HotModuleReplacementPlugin 开启HMR:

const webpack = require('webpack');
module.exports = {
	plugins: [
    	new webpack.HotModuleReplacementPlugin()
    ],
    devServer: {
    	hot: true
    }
}

在本地开发环境下,浏览器是客户端,webpack-dev-server (WDS) 相当于我们的服务器。HMR 的核心就是客户端从服务端拉取更新后的资源(准确地说,HMR 拉取的不是整个资源文件,而是 chunk diff, 即chunk需要更新的部分。

实际上,WDS 与浏览器之间维护了一个 websocket,当本地资源发生变化时,WDS 会向浏览器推送更新事件,并带上这次构建的 hash,让客户端与上一次资源进行对比。客户端根据 hash 值对比文件是否发生变化。如果有了差别,客户端就会向 WDS 发起一个请求,来获取文件的列表,即哪些模块有了改动。客户端就可以再借助这些信息继续向WDS获取chunk的增量更新。

客户端得到chunk的更新后,有一个非常重要的问题,即客户端如何处理这些增量更新?哪些状态需要保留,哪些又需要更新?可以通过相关的API(如 module.hot.accept) 根据自身场景进行处理。像 react-hot-loader 和 vue-loader 也都是借助这些 API 来实现的 HMR。

webpack 构建流程

Webpack的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

  • 初始化参数: 从配置文件和Shell语句中读取与合并参数,得到最终的参数

  • 开始编译: 用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译

  • 确定入口: 根据配置中的 entry 找到所有的入口文件

  • 编译模块: 从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。

  • 完成模块编译: 在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。

  • 输出资源: 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk, 再把每个 Chunk 转换为一个单独的文件加载到输出列表,这步是可以修改输出内容的最后机会。

  • 输出完成: 在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入文件系统。

在以上过程中,Webpack 会在特定的时间点广播特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。

简单说:

  • 初始化: 从启动构建,读取与合并配置参数,加载 Plugin, 实例化 Compiler

  • 编译: 从 Entry 出发,针对每个 Module 串行调用对应的 Loader 去翻译文件的内容,再找到该 Module 依赖的 Module ,递归地进行编译处理

  • 输出: 将编译后的 Module 组合成 Chunk,将 Chunk 转换成文件,输出到文件系统中。 源码分析文章:juejin.cn/post/684490…

module chunk bundle

  • module: 各个源文件,webpack 中一切皆模块
  • chunk:多个模块合并成的,可以设置chunk的地方如 entry、import()、splitChunks
  • bundle: 最终输出的文件

优化打包效率

  • 优化 babel-loader: 开启缓存,明确打包范围
  • IgnorePlugin: 避免引用无用模块
  • noParse 避免重复打包
  • happyPack 多进程打包
  • ParallelUglyPlugin 多进程压缩 JS
  • 自动刷新
  • 自动更新(新代码生成,网页不会刷新,状态不会丢失):webpack-dev-server 和 HotModuleReplacementPlugin
  • DllPlugin:同一个版本只构建一次,不用每次都重新构建。

优化产出代码

  • 小图片通过 url-loader 转为 base64 编码
  • bundle 加 hash 命中缓存(contentHash)
  • 懒加载
  • 提取公共代码(splitChunks)
  • IgnorePlugin
  • 使用 CND 加速
  • 使用 production 模式
  • Scope Hoisting: 代码体积更小;创建函数作用域更少;代码可读性更好。(ModuleConcatenationPlugin)
  • gzip 压缩 (CompressionWebpackPlugin)

Javascript高阶

事件循环

JS的任务分为同步任务和异步任务:

  • 任务队列分为同步任务队列和异步任务队列;
  • 代码执行时,遇到同步代码,会被直接推入同步任务队列并依次执行;
  • 遇到异步代码(如setTimeout、setInteval)会被直接推入异步任务队列;
  • 当同步任务队列执行完毕,这个时候异步任务队列的任务会被一次推入同步任务队列并依次执行 JS的任务队列分为: 宏任务:setTimeout setInterval 微任务:Promise.then方法。注意 new Promise()的时候是同步的,会立即执行。

注意:现在有三个队列:同步队列(也称为执行栈)、宏任务队列、微任务队列 所以,针对这种机制,js的事件循环机制应该是这样的:

  • 遇到同步代码,依次推入同步队列并执行
  • 当遇到setTimeout setInterval,会被推到宏任务队列
  • 如果遇到.then,会被当做微任务,被推入微任务队列
  • 同步队列执行完毕,然后去微任务取任务,直到微任务队列清空。然后检查宏任务队列,去宏队列取任务,并且每一个宏任务执行完毕都会去微任务队列跑一遍,看看有没有新的微任务,有的话再把微任务清空。这样依次循环。

同步代码—> 微任务(要全部执行)—>宏任务(执行一个)—>微任务(全部执行)—>宏任务(执行一个)

  console.log('script start')

  async function async1() {
    await async2()
    console.log('async1 end')
  }
  async function async2() {
    console.log('async2 end')
  }
  async1()

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

  new Promise(resolve => {
    console.log('Promise')
    resolve()
  })
    .then(function() {
      console.log('promise1')
    })
    .then(function() {
      console.log('promise2')
    })

  console.log('script end')
上述代码执行顺序:   
  script start  
  async2 end   
  Promise   
  script end  
  async1 end  
  promise1  
  promise2  
  setTimeout  

说明:async function async1(){...} 函数体内的同步代码其实相当于 new Promise(resolve=>{...; resolve()}) 的代码。是同步代码。遇到 await 相当于 new Promise().then(res=>{...}); 是 微任务,会被放入微任务队列中,等待执行。这个和我的另一篇博文中解释的是一致的 juejin.im/post/688367…
可以再看这个自制的例子:

  async function asyncf1() {
    console.log("async1 start");
    await asyncf2();
    console.log("async1 middle")
    return "hello async"
  }

  async function asyncf2() {
    console.log("async2 start");
    return "hello async222"
  }
  asyncf1().then(res=>{
    console.log(res);
  });

  console.log("tongbu");

执行顺序:

  async1 start
  async2 start
  tongbu
  async1 middle
  hello async

总是不理解 asyncf1() 的执行时 asyncf2 同步执行逻辑。可以换成 promise 写法帮助理解:

function asyncf1() {
    return new Promise((resolve, reject) => {
        console.log("async1 start");
        Promise.resolve(asyncf2()).then(() => {
            console.log("async1 middle");
            resolve("hello async");
        });
    });
}

Promise应用

手写Promise

juejin.cn/post/691571…

Promise加载图片

    function loadImg(url){
      return new Promise((resolve, reject) => {
        var imgDom = document.createElement("img");
        imgDom.src = url;
        //图片加载成功回调
        imgDom.onload = ()=>{
          resolve(imgDom);
        }
        //图片加载失败回调
        imgDom.onerroe = (error)=>{
          reject(error)
        }
        document.body.appendChild(imgDom);
      })
    }
    const url = "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1603026217111&di=fb11837b4633e99c0b71ff48b5213cf1&imgtype=0&src=http%3A%2F%2Fa0.att.hudong.com%2F56%2F12%2F01300000164151121576126282411.jpg";
    loadImg(url).then(res=>{
      console.log(res.width);
    }, error=>{
      console.log(error)
    })

Promise原生XHR请求

   function promiseGet(url) {
        return new Promise((resolve, reject)=>{
          const xhr = new XMLHttpRequest();
          xhr.open('GET', url, true);
          xhr.send();
          xhr.onreadystatechange = function () {
            // 4 响应已完成;  0:请求未初始化,还没有调用 open()。 1:请求已经建立,但是还没有发送,还没有调用 send()。
            // 2:请求已发送,正在处理中(通常现在可以从响应中获取内容头)。 3:请求在处理中;
            if(xhr.readyState === 4){
              // 成功
              if(xhr.status===200){
                resolve(xhr.response);
              }
            }
          }
          xhr.onerror = ()=>{
            reject(xhr.response);
          }
          xhr.upload.onprogress = function (e) {
           const percent = (e.loaded / e.total) * 100;
           console.log("percent: " + percent)
          }
        })
    }
    const url = "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1603026217111&di=fb11837b4633e99c0b71ff48b5213cf1&imgtype=0&src=http%3A%2F%2Fa0.att.hudong.com%2F56%2F12%2F01300000164151121576126282411.jpg";
    promiseGet(url).then(res=>{
      console.log(res);
    }).catch(error=>{
      console.log("error...", error)
    })

手写发布-订阅模式

class EventHub{
    cache = {};
    on(eventName, fn){
    	this.cache[eventName] = this.cache[eventName] || [];
        this.cache[eventName].push(fn);
    }
    emit(eventName, data){
    	(this.cache[eventName]||[]).forEach(fn=>fn&&fn(data));
    }
    off(eventName, fn){
    	const index = indexOf(this.cache[eventName], fn);
        if(index==-1){
        	return;
        }
        this.cache[eventName].splice(index, 1);
    }
}
function indexOf(arr, item){
	if(arr===undefined){
    	return -1;
    }
    let index = -1;
    for(let i=0;i<arr.length;i++){
    	if(arr[i]===item){
        	index = i;
            break;
        }
    }
    return index;
}

Vue

省略缩写

v-bind缩写

  <!-- 完整语法 -->
  <a v-bind:href="url"></a>
  <!-- 缩写 -->
  <a :href="url"></a>

v-on缩写

  <!-- 完整语法 -->
  <a v-on:click="doSomething"></a>
  <!-- 缩写 -->
  <a @click="doSomething"></a>

v-show 和 v-if 的区别

  • v-show 是CSS切换, display: none; v-if是完整的销毁和重新创建, v-if="false"的时候不会创建
  • 频繁切换是用v-show;运行时较少改变时用 v-if

绑定 class 的方法

  1. 对象方法 v-bind:class="{'orange': isRipe}" //orange是class的名称,isRipe是变量
  2. 数组方法 v-bind:class="['class1', class2]" //class1是class名称,class2是变量
  3. 内联 v-bind:style="{color: color}"

组件data为什么是函数

为什么组件中的data必须是一个函数,然后return一个对象,而new Vue实例里,data可以是一个对象?

因为组件是用来复用的,JS里对象是引用类型,这样作用域没有隔离;而new Vue的实例,是不会被复用的,因此不存在引用对象的问题。

watch 和 computed

  • watch: 当一条数据变化影响多条数据的时候用watch;例子:搜索数据
    • wacth的配置项有:handler deep immediate
    • deep: 是否深度监听
    • immediate:是否立即执行;为true 表示当值第一次绑定的时候,也要执行监听函数。比如当父组件向子组件动态传值时,子组件props首次获取到父组件传来的默认值时,也需要执行函数,此时就需要将immediate设为true
  • computed: 当一个属性受到多个属性影响的时候用compute;例子:购物车结算

keep-alive

keep-alive 是Vue内置组件,主要用于保留组件状态或避免重新渲染。

  • Props:
    • include:字符串或正则表达式。只有名称匹配的组件会被缓存。
    • exclude:字符串或正则表达式。任何名称匹配的组件都不会被缓存。
  <keep-alive include="a">
   <component>
    <!-- name 为 a 的组件将被缓存! -->
   </component>
  </keep-alive>可以保留它的状态或避免重新渲染

  <keep-alive exclude="a">
   <component>
    <!-- 除了 name 为 a 的组件都将被缓存! -->
   </component>
  </keep-alive>可以保留它的状态或避免重新渲染

  <keep-alive include="a">
    <router-view>
      <!-- 只有路径匹配到的视图 a 组件会被缓存! -->
    </router-view>
  </keep-alive>

v-model

v-model是语法糖,一个组件上的 v-model 等价于 prop: value 和 input 事件。

  <testComp v-model="initMsg"></testComp>
  //等价于
  <testComp v-bind:value="initMsg" v-on:input="initMsg=$event"></testComp>

单向数据流

所有prop都是得其父子prop之间形成了一个单向下行绑定:父级prop的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外修改父组件的状态。 额外的,每次父组件发生变更时,子组件中所有prop都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变prop。如果你这样做了,Vue会在浏览器控制台中发出警告。

组件通信

父子组件

父组件通过 props 将数据传递给子组件,子组件通过 事件 向父组件发送信息。

  //子组件 Item.vue
  export default {
    name: "Item",
    props: {
      content: {
        type: String,
        default: "item"
      }
    },
    methods: {
      change(){
        this.$emit('changeContent', "new msg");
      }
    }
  }
  <!-- 父组件 -->
  <Item :content="content" v-on:changeContent="fChange"></Item>

父组件 v-on 订阅事件changeContent,子组件 $emit 触发事件,发送信息。

任意组件

利用 new Vue() 出来一个中转站 event ,组件通过 event.$emit("fnName", data) 发布事件; 其他组件组件通过 event.$on("fnName", callback)订阅事件 fnName,并触发回调函数 callback

//event.js
import Vue from 'vue';
export default new Vue();

发布事件:

   //组件A
  change(){
    event.$emit("changeVersion", "3.0");
  },

其他任意组件订阅changeVersion事件,并记得善后工作在 beforeDestroy事件中取消订阅。

  mounted(){
    event.$on("changeVersion", this.alertVersion)
  },
  beforeDestroy(){
    event.$off("changeVersion")
  },

生命周期

  • 创建前后 beforeCreate/created
    • created data和methods都已经初始化好了;如果要用methods中的方法或者操作data中的数据,最早可以在这个阶段中操作
  • 载入前后 beforeMount/mounted
    • mounted 表示Vue实例已经初始化完成了,此时组件脱离了创建阶段,进入到了运行阶段。如果我们想要通过插件操作页面上的DOM节点,最早可以在这个阶段进行。
  • 更新前后 beforeUpdate/update
    • beforeUpdate: 当执行这个钩子时,页面中的显示的数据还是旧的,data中的数据是更新后的, 页面还没有和最新的数据保持同步
    • updated:页面显示的数据和data中的数据已经保持同步了,都是最新的
  • 销毁前后 beforeDestory/destroyed
    • beforeDestory:Vue实例从运行阶段进入到了销毁阶段,这个时候上所有的 data 和 methods , 指令, 过滤器 ……都是处于可用状态。还没有真正被销毁
    • destroyed: 这个时候上所有的 data 和 methods , 指令, 过滤器 ……都是处于不可用状态。组件已经被销毁了。

父子组件生命周期

加载渲染过程

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

子组件更新过程

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

销毁过程

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

beforeDestroy 需要做什么

  • 解绑自定义事件 event.$off
  • 清除定时器
  • 解绑自定义的 DOM 事件,如 window 上面的事件。

检测变化的注意事项

关于 Object 的问题
在 Vue2.0 版本中,对象数据的响应原理是利用 Object.defineProperty 去追踪对象的 key 的内容是否被修改,无法追踪到新增属性和删除属性。

var vm = new Vue({
	el: '#el',
    methods: {
    	action(){
        	//检测不到
        	delete this.obj.name;
            //检测不到
            this.obj.age = 12;
        }
    },
    data: {
    	obj: {
        	name: 'zhangsan'
        }
    }
})

关于 数组 的问题
在 vue2.0 版本中,数组的响应式原理是拦截了数据的7个方法(包括 push、pop、shift、unshift、 splice、 sort、 reverse ),而不是根据 Object.defineProperty 去监听每个 key(数组是序号)对应内容的修改。那么我们直接去修改数组下标对应的内容以及利用 length 去修改数组的长度是不能被追踪到的,无法实现数据的动态响应的。

	//无法被追踪监测到
    this.list[0] = 12;
    //无法被追踪检测到
    this.list.length = 0;

在 vue2.0 的源码中,数组的响应原理如下:

//工具函数
function def(obj, key, val, enumerable){
    Object.defineProperty(obj, key, {
        value: val,
        enumerable: !!enumerable,
        writable: true,
        configurable: true
    })
}
const hasProto = '__proto__' in {};
const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function(method){
    const original = arrayProto[method];
    Object.defineProperty(arrayMethods, method, {
        value: function mutator(...args){
            console.log("here")
            return original.apply(this, args);
        },
        enumerable: false,
        writable: true,
        configurable: true
    })
});
const arrayKeys = Object.getOwnPropertyNames(arrayMethods);

function copyAugment(target, src, keys){
    for(let i=0;i<keys.length;i++){
        const key = keys[i]
        def(target, key, src[key]);
    }
}

const arr = [2, 5, 13, 90];
if(Array.isArray(arr)){
	//支持 __proto__
    if(hasProto){
        arr.__proto__ = arrayMethods;
    } else{
        copyAugment(arr, arrayMethods, arrayKeys);
    }
}

arr.push(4);
console.log(arr);
console.log(arrayMethods);

可以看到根据浏览器是否支持 __proto__的写法分为了 2 种情况。如果浏览器支持__proto__的写法,会将拦截的 7 种方法通过 arr._proto_ = arrayMethods; 将7种拦截方法放在 arr 的原型对象上。不过不支持的话,则直接将 7 种拦截方法设置在 arr 对象上。这样,用户通过数组的 7 种方法任意一种操作数组的时候都是可以被监听到。

$nextTick

Vue 在观察到数据变化时并不是直接更新 DOM,而是开启一个队列(微任务),去更新 DOM 。 所以如果你用一个for循环来动态改变数据100次,其实它只会应用最后一次改变,如果没有这种机制,DOM就要重绘100次。(Vue.js使用异步更新队列更新DOM)

$nextTick 接收一个回调函数作为参数,它的作用是将回调延迟到下次 DOM 更新周期之后执行。将回调函数放入异步队列中。Vue会根据当前浏览器环境优先使用原生的 Promise.then、 MutationObserver 和 setImmediate, 如果都不支持,就会采用 setTimeout 替换。

在 vue2.5 的源码中,macrotask 降级的方案依次是:setImmediate、MessageChannel、setTimeout

vue 的 nextTick 方法的实现原理:

  • vue 用异步队列的方式来控制 DOM 更新和 nextTick 回调先后执行
  • microtask 因为其高优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕
  • 考虑兼容问题, vue 做了 microtask 向 macrotask 的降级方案
  //模拟 nextTick
  const callbacks = [];
  //变量控制 microTimerFunc 一次事件循环中只执行一次。
  let pending = false;
  function flushCallbacks(){
      pending = false;
      const copies = callbacks.slice(0);
      callbacks.length = 0;
      for(let i = 0;i < copies.length; i++){
          copies[i]()
      }
  }
  let microTimerFunc;
  const p = Promise.resolve();
  microTimerFunc = ()=>{
      p.then(flushCallbacks)
  }
  function nextTick(cb, ctx){
      callbacks.push(()=>{
          if(cb){
              cb.call(ctx)
          }
      })
      if(!pending){
          pending = true;
          microTimerFunc()
      }
  }
  //测试一下
  nextTick(function(){
      console.log(this.name);
  }, {name: 'Berwin'});
  console.log("start...");
  nextTick(function(){
      console.log(this.name);
  }, {name: 'lisi'});

vue中的diff算法

vue的数据检测原理是可以知道哪里用到了某个数据,数据变化的时候可以直接通知到对应的 watcher 进行修改。那为什么还需要用 diff 算法呢?因为粒度太细,会有很多 watcher 同时观察某个状态,会有一些内存开销以及一些依赖追踪的开销,所以 Vue.js 2.0 采用了一个中等粒度的解决方案,状态侦测不再细化到某个具体节点,而是组件,组件内部通过虚拟DOM来渲染视图,这可以大大缩减依赖数量和 watcher 数量。

什么是虚拟节点?
虚拟节点(vnode)是 Javascript 中一个普通的对象,这个对象的属性上保存了生成 DOM 节点所需要的一些数据。

在 Vue.js 2.0 版本中组件更新渲染的时候,会使用新创建的虚拟节点和将上一次渲染时缓存的虚拟节点进行对比,然后根据对比结果只更新需要更新的真实DOM节点,从而避免不必要的 DOM 操作,节省一定的性能。这个是对比算法是 diff 算法。

在采取diff算法比较新旧节点的时候,比较只会在同层级进行, 不会跨层级比较。

<div>
    <p>123</p>
</div>

<div>
    <span>456</span>
</div>

上面的代码会分别比较同一层的两个div以及第二层的p和span,但是不会拿div和span作比较。在别处看到的一张很形象的图:

diff 算法源码执行函数依次是:patch (oldVnode, vnode) -> patchVnode (oldVnode, vnode) -> updateChildren (parentElm, oldCh, newCh)

更多细节: juejin.cn/post/684490…

为何在 v-for 中使用 key

根据上面的 diff 算法过程我们知道, vue 在会根据虚拟DOM的tagkey 来决定是否可以复用真实的 DOM 节点。如果在 v-for 中没有使用 key,则 diff 算法只会根据 tag 来判断是否复用真实的 DOM 节点。如果 tag没变,内容修改了,这个时候会有问题,vue 会傻傻的以为真实DOM是可用的,会直接复用之前的真实DOM。看下面的例子:

这个例子中由span、input、button三个标签生成的组件 。 v-for 中没有使用 key,在更新页面的时候,旧的虚拟 dom 是 [1, 2, 3], 新的是 [1, 3] 。第一条数据会被复用;第二条数据由于每个组件没有唯一的标志 id 作为 keydiff 算法过程会先对比旧的虚拟 dom 的第二条数据和新虚拟 dom 的第二条数据,tag 一样会直接复用真实的 dom 节点。导致页面信息错误。

So, v-for 中使用 key 可以:

  1. 在虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNode。
  2. 如果不使用 key,Vue 会使用一种最大限度减少新增或删除元素并且尽可能地尝试修复/再利用相同类型元素的算法。(就地复用tag相同的真实DOM元素)。
  3. 使用 key, 则直接复用 key 值相同的元素。
  4. 带 key 的组件能够触发过渡效果,以及触发组件的声明周期。 细节 如果要标识组件的独特性,让 diff 算法更准确,请使用 独特的 key 而不是使用 for 循环中的 index 或者使用 random。

Vue 的渲染过程

初次渲染:

  • 解析模板为 render 函数
  • 触发响应式,监听 data 属性的 getter setter
  • 执行render 函数,会生成 vnode 并且渲染出页面 更新渲染:
  • 修改 data,触发 setter
  • 重新执行render 函数,生成新的 vnode
  • diff 算法对比新旧vnode ,更新页面

Vuex 原理

Vuex 把组件的共享状态抽出来,以一个全局单例模式管理。每一个 Vuex 应用的核心就是 store (仓库)。 store 基本上就是一个容器,它包含着你的应用中的大部分 状态 (state). Vuex 和单纯的全局对象有以下两点不同:

  1. Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应的得到高效更新。
  2. 你不能直接修改 store 中的状态。改变 store 中的状态的唯一途径就是显示地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态地变化。
    Vuex 实现原理是将 state 的数据通过 new Vue() 后,将数据转为响应式的。同时,将 getter 里面定义的数据通过 new Vue 的 computed 实现了计算属性的特点,只有当它的依赖值发生了改变才会被重新计算。

利用 Vue 自己实现 Vuex
我们可以自定义一个 MyStore.js, 里面定义项目共享的数据项以及修改数据的方法。然后挂载在根 data 中。

// MyStore.js
export default class{
	constructor(){
    	this.count = 10;
        let self = this;
        this.mutations = {
        	incrementCount(){
            	self.count++;
            }
        }
    }
}

项目中引入:

import MyStore from './MyStore.js';
var app = new Vue({
	el: "#app",
    data: {
    	myStore: new MyStore()
    }
})

这样,我们在组件中可以通过 this.$root.myStore.count 访问到全局变量 count 了。通过 this.$root.myStore.mutations.incrementCount(); 进行变量的修改。

Vuex 中 action 和 mutation 的区别

  • action 中处理异步操作,mutation 最好不要。( mutation 处理异步操作页面数据会修改,但是 devtools 里面的值还是原来的并没有修改。出现了数据不一致,无法追踪数据变化。)
  • mutation 做原子操作
  • action 可以整合多个 mutation

Vue Router

hash 模式

Vue Router 默认是 hash 模式,根据 URL 中 hash 值的变化切换组件,达到切换模块的作用。同时整个页面没有刷新。

const router = new VueRouter({
    mode: 'hash',
    routes: [...]
})

hash模式路由的原理是监听windowhashchange事件,URL中hash值的变化会触发该事件。并且我们刷新页面的时候,并不会向后端发起新的资源请求, /index.html#user 请求的地址与/index.html 是一样的。

<p id="content">default</p>
<script>
    window.location.hash = "qq";
    window.addEventListener('hashchange', ()=>{
        document.getElementById("content").innerHTML = window.location.hash;
    })
</script>

history 模式

如果觉得 hash 模式的 # 比较丑的话,Vue Router 还提供了 history 模式

const router = new VueRouter({
    mode: 'hash',
    routes: [...]
})

原理是利用 HTML5 中 history 提供的 pushState、replaceState 这两个 API。它们提供了操作浏览器历史栈的方法。pushState、replaceState 能够在不加载页面的情况下改变浏览器的URL

  <button id="changePage1">page1</button>
  <button id="changePage2">page2</button>
  <script>
      document.getElementById("changePage1").addEventListener("click", ()=>{
          const state = {name: 'page1'};
          history.pushState(state, '', 'page1')
      });

      document.getElementById("changePage2").addEventListener("click", ()=>{
          const state = {name: 'page2'};
          history.replaceState(state, '', 'page2')
      });

      window.onpopstate = (event)=>{
          console.log('onpopstate', event.state, location.pathname);
      }
  </script>

通过pushState和replaceState虽然能改变URL,但是不会主动触发浏览器reload。pushState、replaceState 的区别是 history.pushState()是新增历史记录条目; history.replaceState()是修改(替换)当前历史记录条目。

可以看下面的例子:从 history.html,操作 pushState() 将 page1 添加到历史记录中;再操作 replaceState() 将 page2 替换掉 page1。这个时候点击浏览器返回按钮可以发现,直接返回到 history.html 页面中,并没有返回到 page1 页面。可以看出history.replaceState()是替换历史记录的作用。

window.onpopstate 事件监听的是浏览器前进、后退事件。

页面刷新的时候,history 模式下会向服务器请求新的地址 '127.0.0.1:5500/page1' 这个资源。如果后端没有做相应的处理,浏览器会报 404 错误。需要后端配合,将找不到的页面定位到项目资源的html地址如index.html。同时,由前端路由控制 404 页面显示的内容。

developer.mozilla.org/zh-CN/docs/…

动态路径参数

方式一: params

我们经常需要把某种模式匹配到的所有路由,全都映射到同个组件。例如,我们有一个 User 组件,对于所有 ID 各不相同的用户,都要使用这个组件来渲染。那么,我们可以在 vue-router 的路由路径中使用“动态路径参数”(dynamic segment) 来达到这个效果:

const User = {
	template: '<div>User</div>'
}
const router = new VueRouter({
	routes: [
        //动态路径参数 以冒号开头
    	{ path: 'user/:id', name: 'user', component: User}
    ]
})

现在呢,像 /user/foo 和 /user/bar 都将映射到相同的路由。

一个“路径参数”使用冒号 : 标记。当匹配到一个路由时,参数值会被设置到 this.$route.params,可以在每个组件内使用。于是,我们可以更新 User 的模板,输出当前用户的 ID:

const User = {
	template: '<div>{{ $route.params.id }}</div>'
}

使用:

this.$router.push({name: 'user', params: {id: "zs"}});

http://localhost:8080/#/user/zs 参数出现在路径中

方式二:query

this.$router.push({name: 'list', query: {name: 'William'}})

使用

<div>List:{{$route.query.name}}</div>

http://localhost:8080/#/list?name=William 参数会以?a=b的形式显示

路由懒加载

主要是利用 import() 函数实现。

const router = new VueRouter({
  routes: [
    // 动态路径参数 以冒号开头
    { path: '/user/:id', component: ()=>import(/* webpackChunkName: "User"*/'./components/User.vue') }
  ]
})

$router 和 $route

  • $router 指的是整个路由对象;可以使用 this.$router.push({name: 'user'}) 去跳转页面。
  • $route 指的是当前页面的路由对象;可以使用 this.$route.params.id 或者 this.$route.query.id 获取当前路由对象传递进来的参数

导航守卫

全局前置守卫

const router = new VueRouter({...});

router.beforeEach((to, from, next)=>{
   //...
})
  • to: Route 对象,即将要进入的目标 路由对象
  • from:Route 对象,当前导航正要离开的路由
  • next: Function 函数,继续执行的函数。
    • next(); 继续向下执行
    • next({ path: '/'}); 跳转到指定的路由地址

TypeScript

TypeScript的特点

  • 类型检查。TypeScript在编译代码时进行严格的静态类型检查。这就意味着在编码阶段可能存在的隐患,不必把它们带到线上。

  • 语言扩展。TypeScript会包括ES6和未来提案中的特性,比如异步操作和装饰器。也会从其他语言借鉴某些特点,比如接口和抽象类。

  • 工具属性。TypeScript 可以编译成标准的 Javascript。可以在任何浏览器和操作系统上运行。无需任何运行时额外开销。从这个角度讲TypeScript更像时一个工具。

类型

TypeScript中定义了布尔值boolean、数字number、字符串string、数组Array、元组、枚举、接口、any、void等。

  • 元组。元组表示的是一个已知元素数量和类型的数组,各元素的类型不必相同。
let x: [string, number, boolean] = ['hello', 100, true];
  • 接口。interface 来定义一个接口,接口可以理解成描述了一种数据类型。比如我们定义了一个方法要接受的参数,这个参数必须有哪些属性或者方法。这个时候就可以使用接口定义方法的参数了。
interface params{
	search: string;
    page?: number;
    size?: number;
}

function foo(p: params): string{}
  • 泛型。泛型保证了类型的非确定性和一致性。比如在函数中,我们为了保证函数返回值类型和输入变量类型一致,我们可以使用泛型。

加餐 type 和 interface 的区别:

  • type可以声明 基本类型,联合类型,元组 的别名,interface不行
  • type 语句中可以使用 typeof 获取类型实例
  • type 支持类型映射,interface不支持
  • interface能够声明合并,type不能
    www.cnblogs.com/mengff/p/12…

模块和命名空间

  • TypeScript 中模块的用法和 ES6 的 Module 保持一致。使用 export 语法导出模块的变量和方法;使用 import 引入其他模块的变量和方法。

  • 命名空间。TypeScript 中使用 namespace 关键字来实现命名空间。比如在 Shape 命名空间下的变量只有在该命名空间下可以访问,如果要在全局访问的变量和方法,要通过 export 关键字导出。

  • 命名空间可以拆分为几个文件。

  • 命名空间最终被编译为一个全局变量和一个立即执行函数。

最后, 命名空间不要和模块混用 ,同时命名空间的使用最好在一个全局的环境中使用

声明文件

如果我们想在 TypeScript 中使用第三方类库如 jQuery、lodash等,需要提供这些类库的声明文件(以.d.ts结尾),对外暴露API。一般我们通过安装第三方类库的类型声明包后,即可在 TypeScript 中使用。以 jQuery 为例:

npm install -D jquery @types/jquery

TS 编译流程

和Babel以及其他编译到 Javascript 的工具类似, TS 的编译流程包含一下三步:

解析 -> 转换 -> 生成

包含了一下几个关键部分:

  • Scanner:从源码生成 Token
  • Parser: 从 Token 生成 AST
  • Binder:从AST 生成 Symbol
  • Checker:类型检查
  • Emitter: 生成最终的 JS 文件

引自:juejin.cn/post/684490…

关于 TypeScript 的更多具体内容:juejin.cn/post/690313…

感谢

如果有错误或者不严谨的地方,烦请给予指正,十分感谢。如果喜欢或者有所启发,欢迎点赞,对作者也是一种鼓励。