es6、ts、高阶js的踩坑之旅
# 大前端培训课程笔记
标签(空格分隔): es6 ts node vue react
第一阶段:JS深度剖析
模块一:ES 新特性与 JS 异步编程、TypeScript
任务一: ES新特性
- for循环两层嵌套作用域,for块本身一层,内部循环体一层。
- 最佳实践:不用var,主用const,配合let。
- 对象解构可设别名和默认值:
{name: objName = "jack"} = obj
。 - 带标签的模板字符串:
func`${name} is a ${gender}`
,作用如在func中做gender的翻译处理。 - ES6字符串实用扩展方法:startsWith()、endsWith()、includes()。
- 函数形参设默认值最好放在最后,若不是最后,调函数传参必须传入undefined才能使默认值生效。
foo(first, ...args)
,..args表示剩余参数->数组,只能出现在参数列表的最后。arr2 = [...arr1]
和slice()类似,是对没有引用对象元素的数组的深拷贝,但是有的话是浅拷贝。- 对象字面量增强:a.变量名属性名相同,写一遍加逗号;b.属性方法直接func()定义;c.对象动态属性用[]直接定义(计算属性名)。
Object.assign(target, source1, source2); Object.is(NaN, NaN);
- Proxy的handler方法:(Reflect api雷同)
- Proxy 对比 Object.defineProperty()的优势:a.Proxy 可以监视读写以外的操作;b.Proxy 可以很方便的监视数组操作;c.Proxy 不需要侵入对象。
- 对象中的键只能是字符串(ES6后还可以是Symbol()),但Map中的键可以是任意类型。
- Symbol()最主要的作用就是为对象添加独一无二的属性名,作为对象的私有属性。
- in 、Object.keys()、Json.stringfy()均会忽略对象的Symbol类型的属性。
- 所有可以用for...of循环的结构对象,内部都调用了一个Symbol.iterator方法。
- 迭代器作用:对外提供统一遍历接口(for...of),外部不用关系对象内部数据结构。
- 生成器:* function,用yield返回值,用.next()调用。
- ES2016: a.新方法:array.includes(); b.指数运算符:**。
- ES2017:
Object.values(obj); Object.entries(obj);
Object.getOwnPropertyDescriptors(); Object.defineProperties({}, descriptors)
String.prototype.padStart(); String.prototype.padEnd()
async/await
任务二:JavaScript异步编程
-
Promise写一个ajax:
function ajax(url, method) { return new Promise(function(resolve, reject) { let xhr = new XMLHttpRequest(); xhr.open(method, url); xhr.responseType = "json"; xhr.onload = function() { if (this.status === 200) { resolve(this.response); } else { reject(new Error(this.statusText)); } } xhr.send(); }); } ajax("api/test.json", "get").then(function(value) { console.log(value); }, function(error) { console.log(error); });
-
Promise最常见的错误是嵌套使用(回调地域)。
-
Promise链式调用:Promise对象的then()方法会返回一个全新的Promise对象,后面的then方法就是在为上一个then返回的Promise注册回调,前面then方法中回调函数的返回值会作为后面then方法返回回调的参数,如果回调中返回的是Promise,那后面then方法的回调会等待它的结束。
-
Promise静态方法(直接返回一个Promise对象):
Promise.resolve().then()
/Promise.reject().catch()
-
Promise.then 或者 .catch 的参数期望是函数,传入非函数则会发生值穿透,如下题:
-
Promise.all()内部的全部异步执行完毕后then,Promise.race()内部的异步谁先执行完就then或者catch返回谁(用于实现处理请求超时)。
-
微任务会在当前线程执行完后立即执行,而宏任务要等线程和微任务都结束后才执行,目前绝大多数异步调用是宏任务,Promise是微任务。
-
Generator生成器异步方案:
// 执行生成器的公用函数
function co(generator) {
// 迭代生成器方法
function handleResult(result) {
if (result.done) {
return;// 生成器函数结束
}
result.value.then(data => {
handleResult(g.next(data));
}, error => {
g.throw(error);
});
}
const g = generator();
handleResult(g.next());
}
// 自定义生成器
function * main() {
try{
const data1 = yield ajax("/api/test.json", "get");
console.log(data1);
const data3 = yield ajax("/api/error.json", "get");
console.log(data3);
const data2 = yield ajax("/api/info.json", "get");
console.log(data2);
} catch(e) {
console.log("报错信息:", e);
}
}
// 执行生成器方法,内部的异步请求将按顺序依次往下执行
co(main);
任务三:TypeScript语言
1. 强类型语言的优势(弱类型的缺点):a.错误更早暴露;b.代码更智能;c.重构更牢靠;d.减少不必要的类型判断。
2. Flow:javacript的类型检查器。Flow安装:yarn add flow-bin --dev; 生成Flow配置文件:yarn flow init; 启动/停止Flow服务:yarn flow / yarn flow stop; Flow编译移除注解:a.flow-remove-types;b.babel/perset-flow。
3. ts中文错误提示: yarn tsc --locale zh-CN。
4. 解决作用域问题:a.放到一个立即执行函数中;b.末尾添加 export {}。
5. typescript命令:a.安装:yarn add typescript;b.配置文件:yarn tsc --init;c.编译:yarn tsc;d.调试:yarn add nodemon/yarn add ts-node/yarn nodemon xxx.ts
6. typescript数组类型例子:
function sum(...args: number[]) {
return args.reduce((prev, current) => prev + current, 0);
}
sum(1, 2, 3);
- typescript类型总结(示例):
// 【原始数据类型】
const a: string = 'foobar'
const b: number = 100 // NaN Infinity
const c: boolean = true // false
// 在非严格模式(strictNullChecks)下,
// string, number, boolean 都可以为空
// const d: string = null
// const d: number = null
// const d: boolean = null
const e: void = undefined
const f: null = null
const g: undefined = undefined
// Symbol 是 ES2015 标准中定义的成员,
// 使用它的前提是必须确保有对应的 ES2015 标准库引用
// 也就是 tsconfig.json 中的 lib 选项必须包含 ES2015
const h: symbol = Symbol()
// -------------------------------------------------------------------------
// 【Object 类型】
// object 类型是指除了原始类型以外的其它类型
const foo: object = function () {} // [] // {}
// 如果需要明确限制对象类型,则应该使用这种类型对象字面量的语法,或者是「接口」
const obj: { foo: number, bar: string } = { foo: 123, bar: 'string' }
// -------------------------------------------------------------------------
// 【数组类型】
const arr1: Array<number> = [1, 2, 3]
const arr2: number[] = [1, 2, 3]
// -------------------------------------------------------------------------
// 【元组类型】
// 元组类型是指明确每一个元素类型和元素数量的数组,Object.entries()返回的就是一个元组类型
const tuple: [number, string] = [18, 'zce']
const entries: [string, number][] = Object.entries({
foo: 123,
bar: 456
})
const [key, value] = entries[0];
// 打印entries: [["foo", 123], ["bar", 456]]
// -------------------------------------------------------------------------
// 【枚举类型】
enum PostStatus { // 这种枚举会造成编译时的代码入侵
Draft = 1,
Unpublished = 2,
Published = 3
}
const enum PostStatus { // 常量枚举,不会侵入编译结果
Draft = 1,
Unpublished,
Published
}
// -------------------------------------------------------------------------
// 【函数类型】
function func1 (a: number, b: number = 10, ...rest: number[]): string {
return 'func1'
}
func1(100, 200, 300, 400)
const func2: (a: number, b: number) => string = function (a: number, b: number): string {
return 'func2'
}
// -------------------------------------------------------------------------
// 【接口】
interface Post {
title: string
content: string
}
function printPost (post: Post) {
console.log(post.title)
console.log(post.content)
}
printPost({
title: 'Hello TypeScript',
content: 'A javascript superset'
})
- 类型断言:类型断言不是类型转换,它是用来明确某一个变量的具体类型,其方式有两种:
const num = res as number;
const num2 = <number>res // JSX下不能使用
- interface接口的作用在于约束一个对象的结构,一个对象要实现一个接口,就必须拥有接口中约束的所有成员。
interface Post {
title: string
subtitle?: string // 可选成员
readonly summary: string // 只读成员
}
const hello: Post = {
title: 'Hello TypeScript',
summary: 'A javascript'
}
interface Cache {
[prop: string]: string //动态成员
}
const cache: Cache = {}
cache.foo = 'value1'
- ts中的类对es6的类进行了语法上的增强,跟java很像,如下:
class Person {
public name: string // = 'init name'
private age: number // private表示外部不可访问
// protected表示外部不可以但是子类可以访问;readonly表示不可修改且赋值只能在初始化或构造器
protected readonly gender: boolean
constructor (name: string, age: number) {
this.name = name
this.age = age
this.gender = true
}
sayHi (msg: string): void {
console.log(`I am ${this.name}, ${msg}`)
console.log(this.age)
}
}
class Student extends Person {
private constructor (name: string, age: number) { // 构造器私有表示不能外部实例化
super(name, age)
console.log(this.gender)
}
static create (name: string, age: number) { // 使用静态方法创造实例
return new Student(name, age)
}
}
const jack = Student.create('jack', 18)
- 类的接口与实现:
interface Eat {
eat (food: string): void
}
class Person implements Eat{
eat (food: string): void {
console.log(`优雅的进餐: ${food}`)
}
}
- 抽象类/方法:class/方法名前加abstract:
abstract class Animal {
eat (food: string): void {
console.log(`呼噜呼噜的吃: ${food}`)
}
abstract run (distance: number): void
}
- 抽象类和接口的区别:在typescript中接口和抽象类有什么区别。
- 泛型:
// function createNumberArray (length: number, value: number): number[] {
// const arr = Array<number>(length).fill(value)
// return arr
// }
// function createStringArray (length: number, value: string): string[] {
// const arr = Array<string>(length).fill(value)
// return arr
// }
function createArray<T> (length: number, value: T): T[] {
const arr = Array<T>(length).fill(value)
return arr
}
- Lodash是一个著名的javascript原生库,不需要引入其他第三方依赖。是一个意在提高开发者效率,提高JS原生方法性能的JS库(另有query-string,这些之后要去项目中学习使用起来)。
- typescript中引入第三方模块的时候,可以:a.自己手动用declare声明模块;b.用yarn add @types/模块名 添加声明;c.第三方库有可能已经自己集成兼容ts了。
模块二:函数式编程与 JavaScript 性能优化
任务一:函数式编程范式
-
函数是一等公民是指,函数可以作为参数、可以作为返回值、可以赋值给变量。
-
高阶函数--函数作为参数(回调):
// array的forEach()、filter()、some()、map()、every()等方法,都是高阶函数 // 模拟foreach function forEach(arr, fn) { for (let i = 0; i < arr.length; i++) { fn(arr[i]); } }; let arr = [2,3,5,8,12]; forEach(arr, item => console.log(item)); // 模拟filter function filter(arr, fn) { let results = []; for (let i = 0; i < arr.length; i++) { if (fn(arr[i])) { results.push(arr[i]); } } return results; }; let arr2 = [2,3,5,8,12]; console.log(filter(arr2, item => item % 2 === 0));
-
高阶函数--函数作为返回值(闭包):
// 用心体会这个高阶函数抽象的例子(回调+闭包;可以用来进行并发的处理的公用方法) function once (fn) { let done = false return function () { if (!done) { done = true return fn.apply(this, arguments) } } } let pay = once(function (money) { console.log(`支付: ${money} RMB`) }) pay(5); // 只会执行一次 pay(5); // 这里不会再执行 // 模拟记忆函数,利用纯函数的可缓存(相同输入始终有相同输出)的优点 function memoize(fn) { let cache = {} return function() { let key = JSON.stringify(arguments) cache[key] = cache[key] || fn.apply(this, arguments) return cache[key] } } function getCircleArea(r) { console.log("first exct"); return Math.PI * r * r } let getAreaWithMemory = memoize(getCircleArea); console.log(getAreaWithMemory(4)); // 会打印"first exct" console.log(getAreaWithMemory(4)); // 已有缓存,不会重复执行
-
高阶函数总结:高阶函数就是去抽象通用的问题,实现具体的细节,封装起来,之后我们只需关注我们需要实现的目标,然后直接套用就可以。
-
闭包的本质:函数在执行的时候会放到一个执行栈上,当函数执行完毕后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员。
-
纯函数:相同的输入永远会得到相同的输出,而且没有任何可观察的副作用,例如数组的slice()是纯函数,splice()是不纯函数。
-
纯函数的好处:a.可缓存;b.可测试;c.并行处理。
-
柯里化概念:当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变),然后返回一个新的函数接收剩余的参数,返回结果;柯里化是一种对函数参数的'缓存'。
-
柯里化原理模拟:
// 模拟loadsh柯里化函数的原理,es5写法 function curry(fn) { return function curriedFn(...args) { if (args.length < fn.length) { // return function () { // return curriedFn(...args.concat(Array.from(arguments))) // } return function (...rest) { return curriedFn(...args.concat(rest)) } } return fn(...args) } }
// es6写法 const curry = fn => curriedFn = (...args) => args.length < fn.length ? (...rest) => curriedFn(...args.concat(rest)) : fn(...args) const getSum = (a, b, c) => a + b + c; const getSumCurried = curry(getSum); console.log(getSumCurried(1)(2)(3));
-
函数组合:如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数。
-
组合函数原理模拟:
// es5写法 function compose(...args) { return function (value) { return args.reverse().reduce(function(acc, fn) { return fn(acc) }, value) } } // es6写法 // const compose = (...args) => value => args.reverse().reduce((acc, fn) => fn(acc), value); const reverse = arr => arr.reverse(); const first = arr => arr[0]; const fn = compose(first, reverse); console.log(fn(["jack", "tom", "rose"]));
-
loadsh/fp模块提供了实用的对函数式编程友好的方法,参数函数优先、数据之后。
-
PointFree:只需要定义一些辅助的基本运算函数,然后合成运算过程,不需要指明处理的数据,pointfree案例如下:
// 把一个字符串中的首字母提取并转换成大写, 使用. 作为分隔符 // world wild web ==> W. W. W const fp = require('lodash/fp') // const firstLetterToUpper = fp.flowRight(fp.join('. '), fp.map(fp.first), fp.map(fp.toUpper), fp.split(' ')) const firstLetterToUpper = fp.flowRight(fp.join('. '), fp.map(fp.flowRight(fp.first, fp.toUpper)), fp.split(' ')) console.log(firstLetterToUpper('world wild web'))
-
函子(Functor):是一个特殊的容器,通过一个普通的对象来实现,该对象具有map方法,map方法可以运行一个函数对值进行处理(变形关系)。可以用来在函数式编程中把副作用控制在可控的范围内、异常处理、异步操作等。
-
基本函子:
// Pointed函子:实现了of静态方法的函子 class Container { static of (value) { return new Container(value) } constructor (value) { this._value = value } map (fn) { return Container.of(fn(this._value)) } } // let r = Container.of(5) // .map(x => x + 2) // .map(x => x * x) // console.log(r)
-
MayBe函子:
class MayBe { static of (value) { return new MayBe(value) } constructor (value) { this._value = value } map (fn) { return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value)) } isNothing () { return this._value === null || this._value === undefined } }
-
Either 函子 :
class Left { static of (value) { return new Left(value) } constructor (value) { this._value = value } map (fn) { return this } } class Right { static of (value) { return new Right(value) } constructor (value) { this._value = value } map (fn) { return Right.of(fn(this._value)) } } function parseJSON (str) { try { return Right.of(JSON.parse(str)) } catch (e) { return Left.of({ error: e.message }) } }
-
IO函子,中的_value是一个函数,这里是把函数作为值来处理,可以把不纯的动作存储到 _value中,用来延迟这个不纯的操作,把不纯的操作交给调用者来处理:
const fp = require('lodash/fp') class IO { static of (value) { return new IO(function () { return value }) } constructor (fn) { this._value = fn } map (fn) { return new IO(fp.flowRight(fn, this._value)) } } let r = IO.of(process).map(p => p.execPath) console.log(r._value())
-
Folktale:一个标准的函数式编程库,和loadsh、remda不同,没有提供很多功能函数,而是只提供了一些函数式处理(如compose、curry)和一些函子(Task、Either、MayBe等)。
-
Task函子(处理异步任务):
// Task 处理异步任务 const fs = require('fs') const { task } = require('folktale/concurrency/task') const { split, find } = require('lodash/fp') function readFile (filename) { return task(resolver => { fs.readFile(filename, 'utf-8', (err, data) => { if (err) resolver.reject(err) resolver.resolve(data) }) }) } readFile('package.json') .map(split('\n')) .map(find(x => x.includes('version'))) .run() .listen({ onRejected: err => { console.log(err) }, onResolved: value => { console.log(value) } })
-
Monad函子:可以变扁的Pointed函子,具有join()和of()两个方法,可以用来解决IO函子嵌套过多的问题。
// IO Monad const fs = require('fs') const fp = require('lodash/fp') class IO { static of (value) { return new IO(function () { return value }) } constructor (fn) { this._value = fn } map (fn) { return new IO(fp.flowRight(fn, this._value)) } join () { return this._value() } flatMap (fn) { return this.map(fn).join() } } let readFile = function (filename) { return new IO(function () { return fs.readFileSync(filename, 'utf-8') }) } let print = function (x) { return new IO(function () { console.log(x) return x }) } let r = readFile('package.json') // .map(x => x.toUpperCase()) .map(fp.toUpper) .flatMap(print) .join()
任务二:JavaScript 性能优化
-
内存管理:开发者主动申请空间、使用空间、释放空间。
-
GC算法:垃圾回收机制,常见的有:引用计数、标记清除、标记整理、分代回收。
-
引用计数原理:设置引用数,判断当前引用数是否为0。优点:a.发现垃圾时立即回收;b.最大限度减少程序暂停。缺点:a.无法回收循环引用的对象;b.时间开销大。
-
标记清除原理:遍历并标记活动对象,遍历并清除没有标记的对象。优点:可以解决循环引用不能回收的问题。缺点:回收后的空间是碎片化的,不能使空间得到最大化的使用;不会立即回收垃圾对象。
-
标记整理原理:标记清除的增强,标记阶段一致,清除阶段会先执行整理,移动对象位置。优点:可以解决空间碎片化问题。缺点:不会立即回收垃圾对象。
-
V8是一个js引擎,特点是采用即时编译和内存设限。
-
V8垃圾回收策略:分代回收、空间复制(新生代)、标记整理(新、老生代)、标记清除(老生代)、标记增量(老生代)。
-
界定内存问题的标准:a.内存泄漏:内存使用持续升高;b.内存膨胀:在多数设备上都存在性能问题;c.频繁的垃圾回收:通过内存变化图进行分析。
-
监控内存的几种方式:a.浏览器任务管理器;b.timeline时序图记录;c.堆快照查找分离dom;d.判断是否存在频繁的垃圾回收。
-
基于Benchmark.js的Jsperf可以进行js性能的单元测试。
-
js性能优化策略:
// 【1.慎用全局变量】 function fn() { // name = 'lg' const name = 'lg' console.log(`${name} is a coder`) } // 【2.缓存全局变量】 function getBtn2() { let obj = document let oBtn1 = obj.getElementById('btn1') } // 【3.通过原型对象添加附加方法】 var fn1 = function() { // this.foo = function() { // console.log(11111) // } } fn1.prototype.foo = function() { console.log(11111) } // 【4.避开闭包陷阱】 // 【5.避免属性访问方法使用】 function Person() { this.name = 'icoder' this.age = 18 // this.getAge = function() { // return this.age // } } const p2 = new Person() // const a = p1.getAge() const a = p2.age // 【6.for循环优化】 for (var i = 0,len = arrList.length; i < len; i++) { console.log(arrList[i]) } // 【7.选择最优循环方法】 // foreach > for > forin // 【8.文档碎片优化节点添加】 var oP = document.createElement('p') // document.body.appendChild(oP) const fragEle = document.createDocumentFragment() fragEle.appendChild(oP) document.body.appendChild(fragEle) // 【9.克隆优化节点操作】 var oldP = document.getElementById('box1') for (var i = 0; i < 3; i++) { // var newP = document.createElement('p') var newP = oldP.cloneNode(false) newP.innerHTML = i document.body.appendChild(newP) } // 【10.直接量替换new object()】
第二阶段: 前端工程化实战
模块一: 开发脚手架与自动化构建工作流封装
任务一:工程化概述
-
前端工程化主要解决的问题:
(1) 传统语言或语法的弊端;(2) 无法使用模块化/组件化;(3) 重复的机械式工作;
(4) 代码风格统一、质量保证;(5) 依赖后端服务接口支持;(6) 整体依赖后端项目;
-
工程化表现:一切以提高效率、降低成本、质量保证为目的的手段都属于工程化;
任务二:脚手架工具
-
node的环境变量配置:
-
在node安装路径下新建node_global、node_cache文件夹;
-
在终端运行:
npm config set prefix "D:\Node\nodejs\node_global" npm config set cache "D:\Node\nodejs\node_cache"
-
在系统变量添加 NODE_PATH,值为:D:\Node\nodejs\node_global\node_modules;
-
在用户变量的PATH添加 D:\Node\nodejs\node_global;
-
重启终端。
-
-
yarn的环境变量配置:
-
在D盘新建yarn/global和yarn/cache文件夹;
-
在终端运行:
yarn config set global-folder "D:\yarn\global" yarn config set cache-folder "D:\yarn\cache"
-
终端输入yarn global dir 获取yarn全局安装路径(D:\yarn\global);
-
在NODE_PATH中添加:D:\yarn\global;
-
终端输入yarn global bin 获取yarn 的 bin的 位置(D:\Node\nodejs\node_global\bin);
-
在系统和用户变量的PATH中添加:D:\Node\nodejs\node_global\bin;
-
重启终端。
-
-
脚手架的本质作用:创建项目基础结构、提供项目规范和约定(如相同的组织结构、开发范式、模块依赖、工具配置、基础代码)。
-
Yeoman(通用型脚手架工具)的基础使用:
// 在全局范围内安装yo yarn global add yo // 安装对应的generator yarn global add generator-node // 通过yo运行generator yo node
-
yeoman使用步骤总结:
- 明确你的需求;
- 找到合适的Generator;
- 全局范围安装找到的Generator;
- 通过yo运行对应的Generator;
- 通过命令行交互填写选项;
- 生成你所需要的项目结构;
-
Generator本质上就是一个NPM模块。
-
Plop是一个小而美的脚手架工具,一般用于创建项目中的同类型文件,其需要使用plopfile.js定义生成规则;可以提高开发过程中的效率。
-
脚手架原理:
(1) package.json中定义:"bin":"test-cli.js"
(2) 根目录建test-cli.js文件和templates模板文件夹
(3) 编写test-cli.js:
#!/usr/bin/env node // Node CLI 应用入口文件必须要有这样的文件头 // 如果是 Linux 或者 macOS 系统下还需要修改此文件的读写权限为 755 // 具体就是通过 chmod 755 cli.js 实现修改 // 脚手架的工作过程: // 1. 通过命令行交互询问用户问题 // 2. 根据用户回答的结果生成文件 const fs = require('fs') const path = require('path') const inquirer = require('inquirer') const ejs = require('ejs') inquirer.prompt([ { type: 'input', name: 'name', message: 'Project name?' } ]) .then(anwsers => { // console.log(anwsers) // 根据用户回答的结果生成文件 // 模板目录 const tmplDir = path.join(__dirname, 'templates') // 目标目录 const destDir = process.cwd() // 将模板下的文件全部转换到目标目录 fs.readdir(tmplDir, (err, files) => { if (err) throw err files.forEach(file => { // 通过模板引擎渲染文件 ejs.renderFile(path.join(tmplDir, file), anwsers, (err, result) => { if (err) throw err // 将结果写入目标文件路径 fs.writeFileSync(path.join(destDir, file), result) }) }) }) })
任务三:自动化构建
-
自动化构建的作用:脱离运行环境兼容带来的问题,可以使得在开发阶段使用提高效率的语法、规范和标准。
-
NPM Scripts:实现自动化构建工作流的最简方式;例:
// package.json中的scripts { "scripts": { "build": "sass scss/main.scss css/style.css --watch", "serve": "browser-sync . --files \"css/*.css\"", "start": "run-p build serve" } }
yarn start
-
常用的自动化构建工具:Grunt(生态完善,但基于临时文件、构建速度慢)、Gulp(基于内存、构建速度快、效率高、更方便)、FIS(百度推出,集成比较多,大而全)。
-
Grunt的使用:
-
yarn init --yes
-
yarn add grunt
-
code gruntfile.js
// gruntfile.js // Grunt 的入口文件 // 用于定义一些需要 Grunt 自动执行的任务 // 需要导出一个函数 // 此函数接收一个 grunt 的对象类型的形参 // grunt 对象中提供一些创建任务时会用到的 API module.exports = grunt => { grunt.registerTask('foo', 'a sample task', () => { console.log('hello grunt') }) grunt.registerTask('bar', () => { console.log('other task') }) // // default 是默认任务名称 // // 通过 grunt 执行时可以省略 // grunt.registerTask('default', () => { // console.log('default task') // }) // 第二个参数可以指定此任务的映射任务, // 这样执行 default 就相当于执行对应的任务 // 这里映射的任务会按顺序依次执行,不会同步执行 grunt.registerTask('default', ['foo', 'bar']) // 也可以在任务函数中执行其他任务 grunt.registerTask('run-other', () => { // foo 和 bar 会在当前任务执行完成过后自动依次执行 grunt.task.run('foo', 'bar') console.log('current task runing~') }) // 默认 grunt 采用同步模式编码 // 如果需要异步可以使用 this.async() 方法创建回调函数 // grunt.registerTask('async-task', () => { // setTimeout(() => { // console.log('async task working~') // }, 1000) // }) // 由于函数体中需要使用 this,所以这里不能使用箭头函数 grunt.registerTask('async-task', function () { const done = this.async() setTimeout(() => { console.log('async task working~') done() }, 1000) }) }
- yarn grunt async-task
-
-
Grunt标记任务失败:同步,return false;异步,done(false);一个任务失败后别的任务不会再执行。
-
Grunt的配置方法:
module.exports = grunt => { grunt.initConfig({ foo: { bar: 123 } }) grunt.registerTask('foo', () => { console.log(grunt.config('foo.bar')) }) }
-
Grunt多目标模式,可以让任务根据配置形成多个子任务:
grunt.initConfig({ options: { // 特殊,配置选项 msg: 'task options' }, build: { foo: 100, bar: '456' } }) grunt.registerMultiTask('build', function () { console.log(`task: build, target: ${this.target}, data: ${this.data}`) })
-
Grunt插件的使用:
module.exports = grunt => { grunt.initConfig({ clean: { temp: 'temp/**' } }) grunt.loadNpmTasks('grunt-contrib-clean') }
-
Grunt常用插件使用:sass、babel、watch:
const sass = require('sass') const loadGruntTasks = require('load-grunt-tasks') module.exports = grunt => { grunt.initConfig({ sass: { options: { sourceMap: true, implementation: sass }, main: { files: { 'dist/css/main.css': 'src/scss/main.scss' } } }, babel: { options: { sourceMap: true, presets: ['@babel/preset-env'] }, main: { files: { 'dist/js/app.js': 'src/js/app.js' } } }, watch: { js: { files: ['src/js/*.js'], tasks: ['babel'] }, css: { files: ['src/scss/*.scss'], tasks: ['sass'] } } }) // grunt.loadNpmTasks('grunt-sass') loadGruntTasks(grunt) // 自动加载所有的 grunt 插件中的任务 grunt.registerTask('default', ['sass', 'babel', 'watch']) }
-
Gulp相较Grunt更加高效易用,基本使用如下:
// gulpfile.js // // 导出的函数都会作为 gulp 任务 // exports.foo = () => { // console.log('foo task working~') // } // gulp 的任务函数都是异步的 // 可以通过调用回调函数标识任务完成 exports.foo = done => { console.log('foo task working~') done() // 标识任务执行完成 } // default 是默认任务 // 在运行是可以省略任务名参数 exports.default = done => { console.log('default task working~') done() } // v4.0 之前需要通过 gulp.task() 方法注册任务 const gulp = require('gulp') gulp.task('bar', done => { console.log('bar task working~') done() })
-
Gulp组合任务:
const { series, parallel } = require('gulp') const task1 = done => { setTimeout(() => { console.log('task1 working~') done() }, 1000) } const task2 = done => { setTimeout(() => { console.log('task2 working~') done() }, 1000) } // 让多个任务按照顺序依次执行(例如部署的时候需要先执行编译任务) exports.foo = series(task1, task2) // 让多个任务同时执行(例如编译的时候,css和js的编译可以同时进行) exports.bar = parallel(task1, task2)
-
Gulp处理异步任务方式:回调、promise、async/await、stream。
-
Gulp原理:读取流---->转换流---->写入流。
-
Gulp文件操作API:src, dest:
// gulpfile.js const { src, dest } = require('gulp') const cleanCSS = require('gulp-clean-css') const rename = require('gulp-rename') exports.default = () => { return src('src/*.css') .pipe(cleanCSS()) .pipe(rename({ extname: '.min.css' })) .pipe(dest('dist')) }
-
Gulp自动加载插件:
gulp-load-plugins
。
模块二:模块化开发与规范化标准
任务一:模块化开发
-
模块化标准规范(最佳实践):nodejs ---> CommonJs; 浏览器环境 ---> ES Modules。
-
ES Modules基本特性:
- 通过给script添加type="module"就可以以ES Module的标准执行其中的JS代码;
- ESM自动采用严格模式,忽略 'use strict'(不能全局使用this);
- 每个ESM都是运行在单独的私有作用域中;
- ESM是通过CORS的方式请求外部JS模块的;
- ESM的script标签会延迟执行脚本;
-
ES Modules导出导入: export / import。
-
热更新模块:browser-sync;
-
ES Modules 导出的注意事项:
- export / import后面的{}是固定语法,与es6字面对象和结构无关;
- export导出的是变量的引用,不是变量的复制;
- export导出的是变量是只读的,不可在模块外部修改;
-
ES Modules 导入的注意事项:
-
import from后的路径和名称必须是完整的,不可省略.js和目录路径;
-
相对路径的话不可省略./,且可使用绝对路径或者完整的url;
-
import './module.js'只执行相关的模块文件,不导入变量;
-
import * as modObj from './module.js' 可以全部导出;
-
import不可嵌套在函数中,不可from一个变量,如有此动态加载需求,需使用:
import('.module.js').then(function(module){ console.log(module); })
-
若export同时导出命名成员和默认成员,import可以使用如下方式接收:
// import {name, age, default as title} as modObj './module.js' import title, {name, age} as modObj './module.js'
-
-
ESM可以直接导出导入的成员(index.js常用作用)。
-
browser-es-module-loader.js可以解决ESM浏览器兼容问题。
-
Nodejs的V8.5版本后支持ESM。
-
ESM中可以导入CommonJs模块,CommonJs始终只会导出一个默认成员,不能在CommonJs中通过require载入ESM。
-
ESM中没有CommonJs中的那些模块全局成员了。
任务二:Webpack 打包
-
Webpack:模块打包器、模块加载器、代码拆分、资源模块;打包工具解决的是前端整体的模块化,并不单指JS模块化。
-
Webpack默认从src下的index.js打包到dist下的main.js,可在根目录添加webpack.config.js进行自定义:
// webpack.config.js const path = require('path') module.exports = { // mode有:development/production/none mode: 'development', entry: './src/main.js', output: { filename: 'bundle.js', path: path.join(__dirname, 'output'), publicPath: 'dist/' } }
-
Webpack 资源模块加载:加载除了js外的资源模块,需要使用module加载loader,Loader是Webpack的核心特性。
// webpack.config.js module: { rules: [ { test: /.css$/, use: [ 'style-loader', 'css-loader' ] } ] }
-
Webpack 文件资源加载器 :
// webpack.config.js const path = require('path') module.exports = { output: { publicPath: 'dist/' } } module: { rules: [ { test: /.png$/, use: 'file-loader' } ] }
-
Webpack URL 加载器 :
{ test: /.png$/, use: { loader: 'url-loader', options: { // 小文件使用Data URLS,减少请求次数 ---> url-loader // 大文件单独存放,提高加载速度 ---> file-loader limit: 10 * 1024 // 10 KB } } }
-
Webpack 常用加载器分类 :
- 编译转换类:css-loader、babel-loader
- 文件操作类:file-loader
- 代码检查类:eslint-loader
-
webpack中的es6转换:
{ test: /.js$/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } } },
-
webpack加载资源的方式支持遵循ESM的import、CommonJs的require、AMD的define和require、css-loader中的@import和url、html代码中的img:src和a:herf。
-
对于同一个资源,可以依次使用多个loader,Loaders类似一个管道,最终接收的是一个javascript格式的字符串。
-
Webpack 插件机制 :Loader专注于实现资源模块加载,而Plugin解决其他自动化工作,Plugin拥有更宽的能力范围。
-
常用的一些webpack插件:
- 自动清除输出目录插件:clean-webpack-plugin
- 自动生成Html插件:html-webpack-plugin
- 复制文件插件:copy-webpack-plugin
// webpack.config.js const { CleanWebpackPlugin } = require('clean-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin') const CopyWebpackPlugin = require('copy-webpack-plugin') plugins: [ new CleanWebpackPlugin(), // 用于生成 index.html new HtmlWebpackPlugin({ title: 'Webpack Plugin Sample', meta: { viewport: 'width=device-width' }, template: './src/index.html' }), // 用于生成 about.html new HtmlWebpackPlugin({ filename: 'about.html' }), new CopyWebpackPlugin([ // 'public/**' 'public' ]) ]
-
自己实现webpack插件,需要通过在生命周期的钩子中挂载函数实现扩展,如要实现一个去除打包后js文件中的开头的注释的插件MyPlugin:
// webpack.config.js class MyPlugin { apply (compiler) { compiler.hooks.emit.tap('MyPlugin', compilation => { // compilation => 可以理解为此次打包的上下文 for (const name in compilation.assets) { // console.log(name) // console.log(compilation.assets[name].source()) if (name.endsWith('.js')) { const contents = compilation.assets[name].source() const withoutComments = contents.replace(/\/\*\*+\*\//g, '') compilation.assets[name] = { source: () => withoutComments, size: () => withoutComments.length } } } }) } } module.exports = { mode: 'none', entry: './src/main.js', output: {}, module: { rules: [] }, plugins: [ new MyPlugin() ] }
-
Webpack Dev Server 可以实现自动编译和自动刷新浏览器,基于内存,减少磁盘读写操作。
-
Webpack Dev Server静态资源访问和代理:
devServer: { contentBase: './public', proxy: { '/api': { // http://localhost:8080/api/users -> https://api.github.com/api/users target: 'https://api.github.com', // http://localhost:8080/api/users -> https://api.github.com/users pathRewrite: { '^/api': '' }, // 不能使用 localhost:8080 作为请求 GitHub 的主机名 changeOrigin: true } } },
-
Source Map解决了源代码与运行代码不一致所产生的问题。
-
Webpack 配置 Source Map:
devtool: 'source-map'
。 -
Webpack devtool 模式对比:
- eval - 是否使用eval执行模块代码;
- cheap - Source Map是否包含行信息;
- module - 是否能够得到Loader处理之前的源代码;
- 建议:开发环境--->cheap-module-eval-source-map / 生产环境--->none/nosources-source-map;
-
Webpack HMR,模块热更新,应用运行过程中实时替换某个模块,而不会影响应用运行状态:
devServer: { hot: true // hotOnly: true // 只使用 HMR,不会 fallback 到 live reloading }, plugins: [ new webpack.HotModuleReplacementPlugin() ]
-
Webpack HMR除了css文件外,需要手动去入口文件处理热更新逻辑:
module.hot.accept('./better.png', () => { img.src = background console.log(background) })
-
Webpack 不同环境下的配置有两种方式:
-
配置文件根据环境导出不同配置,webpack.config.js导出一个函数:
module.exports = (env, argv) => { const config = {}, // 放一些公共配置 plugins: [ new HtmlWebpackPlugin({ title: 'Webpack Tutorial', template: './src/index.html' }), new webpack.HotModuleReplacementPlugin() ] } if (env === 'production') { config.mode = 'production' config.devtool = false config.plugins = [ ...config.plugins, new CleanWebpackPlugin(), new CopyWebpackPlugin(['public']) ] } return config }
-
一个环境对应一个配置文件:
// 例如此为生产环境配置文件:webpack.prod.js const merge = require('webpack-merge') const { CleanWebpackPlugin } = require('clean-webpack-plugin') const CopyWebpackPlugin = require('copy-webpack-plugin') const common = require('./webpack.common') module.exports = merge(common, { mode: 'production', plugins: [ new CleanWebpackPlugin(), new CopyWebpackPlugin(['public']) ] })
-
-
webpack.DefinePlugin为代码注入全局成员:process.env.NODE_ENV。
-
Webpack会在生产模式下自动开启Tree Shaking 来去除冗余代码,而在开发环境中,可在配置文件中使用:
optimization: { // 模块只导出被使用的成员(标记无用代码) usedExports: true, // 压缩输出结果(删掉无用代码) minimize: true // 尽可能合并每一个模块到一个函数中 concatenateModules: true }
-
Tree Shaking使用的前提是,由webpack打包的代码必须使用ESM;因此有可能与babel冲突失效,解决方法是babel-loader的配置选项指定modules。
use: { loader: 'babel-loader', options: { presets: [ // 如果 Babel 加载模块时已经转换了 ESM,则会导致 Tree Shaking 失效 // ['@babel/preset-env', { modules: 'commonjs' }] // ['@babel/preset-env', { modules: false }] // 也可以使用默认配置,也就是 auto,这样 babel-loader 会自动关闭 ESM 转换 ['@babel/preset-env', { modules: 'auto' }] ] } }
-
Webpack 代码分割 :
-
多入口打包(多页应用程序)
const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = { mode: 'none', entry: { index: './src/index.js', album: './src/album.js' }, output: { filename: '[name].bundle.js' }, optimization: { splitChunks: { // 自动提取所有公共模块到单独 bundle chunks: 'all' } }, module: { rules: [] }, plugins: [ new HtmlWebpackPlugin({ title: 'Multi Entry', template: './src/index.html', filename: 'index.html', chunks: ['index'] }), new HtmlWebpackPlugin({ title: 'Multi Entry', template: './src/album.html', filename: 'album.html', chunks: ['album'] }) ] }
-
动态导入(动态导入的模块会被自动分包)
if (hash === '#posts') { import(/* webpackChunkName: 'components' */'./posts/posts').then(({ default: posts }) => { mainElement.appendChild(posts()) }) } else if (hash === '#album') { import(/* webpackChunkName: 'components' */'./album/album').then(({ default: album }) => { mainElement.appendChild(album()) }) }
-
-
Webpack MiniCssExtractPlugin提取css到单个文件,Webpack OptimizeCssAssetsWebpackPlugin压缩输出的css文件。
-
Webpack 输出文件名 Hash模式有三种:hash、chunkhash、contenthash:
output: { filename: '[name]-[contenthash:8].bundle.js' },
任务三:其他打包工具
-
Rollup 仅仅是一款ESM打包器,优缺点如下:
优点:
- 输出结果更加扁平
- 会自动移除未引用代码
- 打包结果依然完全可读
缺点:
- 加载非ESM的第三方模块比较复杂
- 模块最终都被打包到一个函数中,无法实现HMR
- 浏览器环境中,代码拆分功能依赖AMD库
-
Webpack大而全,Rollup小而美,应用开发使用webpack,库、框架开发使用Rollup。
-
rollup配置文件:
// rollup.config.js import json from 'rollup-plugin-json' import resolve from 'rollup-plugin-node-resolve' export default { input: 'src/index.js', output: { file: 'dist/bundle.js', format: 'iife' }, plugins: [ json(), resolve() ] } // 运行:yarn rollup --config
-
插件是Rollup唯一扩展途径(加载NPM模块和CommonJs模块)。
-
Rollup代码拆分可使用动态导入方式。
-
Parcel:零配置的前端应用打包器:
// package.json "scripts": { "dev": "parcel src/*.html", "build": "parcel build src/*.html" },
任务四:模块化开发与规范化标准
-
ESLint安装步骤:
- 初始化项目
- 安装ESLint模块为开发依赖
- 通过cli命令验证安装结果
-
eslint的配置文件示例如下:
// .eslintrc.js module.exports = { env: { browser: true, es2020: true }, extends: [ 'standard' ], parser: '@typescript-eslint/parser', parserOptions: { ecmaVersion: 11 }, plugins: [ '@typescript-eslint' ], rules: { } }
-
Stylelint用于检查CSS代码格式,有cli工具,支持sass/less/postCss,支持Gulp/Webpack:
// .stylelintrc.js module.exports = { extends: [ 'stylelint-config-standard', 'stylelint-config-sass-guidelines' ], }
-
Prettier用于格式化代码: npx prettier . --write 。
-
Husky可以实现Git Hooks的使用需求,如提交前的eslint校验,lint-staged可以实现Husky后的代码格式化,二者在package.json中进行配合和配置。
第三阶段:Vue.js框架源码与进阶
模块一:手写 Vue Router、手写响应式实现、虚拟 DOM 和 Diff 算法
任务一:Vue.js 基础回顾
-
vue基础结构:
new Vue({ data: { company: { name: '拉勾', address: '中关村创业大街籍海楼4层' } }, render(h) { return h('div', [ h('p', '公司名称:' + this.company.name), h('p', '公司地址:' + this.company.address) ]) } }).$mount('#app')
-
vue生命周期,见官网。
-
vue中的概念:
- 插值表达式、指令、计算属性和侦听器、Class和Style绑定、条件渲染/列表渲染、表单输入绑定
- 组件、插槽、插件、混入mixin、深入响应式原理、不同构建版本的Vue
任务二:Vue-Router 原理实现
-
Vue Router 基础回顾-使用步骤:
-
Vue.use(VueRouter)注册路由插件;
-
传入路由规则,创建router对象:
export default new Router({ routes: [ { path: '/', name: 'HelloWorld', component: HelloWorld } ] })
-
在main.js中注册router对象:
new Vue({ router, render: h => h(App) }).$mount('#app')
-
创建路由组建的占位:
<router-view/>
-
创建链接:router-link
-
-
动态路由,可以通过prop属性来接收路由参数。
-
Vue的hash模式:
- URL中#后面的内容作为路径地址;
- 监听hashchange事件;
- 根据当前路由地址找到对应的组件重新渲染;
-
Vue的history模式:
- 通过history.pushState()方法改变地址栏;
- 监听popstate事件;
- 根据当前路由地址找到对应的组件重新渲染;
-
Vue路由的history模式需要服务器端支持,否则刷新会报404:
-
Node.js支持history模式:
const path = require('path') // 导入处理 history 模式的模块 const history = require('connect-history-api-fallback') // 导入 express const express = require('express') const app = express() // 注册处理 history 模式的中间件 app.use(history()) // 处理静态资源的中间件,网站根目录 ../web app.use(express.static(path.join(__dirname, '../web'))) // 开启服务器,端口是 3000 app.listen(3000, () => { console.log('服务器开启,端口:3000') })
-
Nginx支持history模式,在nginx.conf中配置:
location / { root html index index.html index.htm try_files $uri $uri/ /index.html; }
-
-
Vue的构建版本:
- 运行版本:不支持template模板,需要打包的时候提前编译;
- 完整版:包含运行时和编译器,体积比运行时版大10K左右,程序运行的时候把模板转换成render函数。
-
模拟一个VueRouter:
let _Vue = null export default class VueRouter { static install(Vue) { //1 判断当前插件是否被安装 if (VueRouter.install.installed) { return; } VueRouter.install.installed = true //2 把Vue的构造函数记录在全局 _Vue = Vue //3 把创建Vue的实例传入的router对象注入到Vue实例 // _Vue.prototype.$router = this.$options.router _Vue.mixin({ beforeCreate() { if (this.$options.router) { _Vue.prototype.$router = this.$options.router } } }) } constructor(options) { this.options = options this.routeMap = {} // observable this.data = _Vue.observable({ current: "/" }) this.init() } init() { this.createRouteMap() this.initComponent(_Vue) this.initEvent() } createRouteMap() { //遍历所有的路由规则 吧路由规则解析成键值对的形式存储到routeMap中 this.options.routes.forEach(route => { this.routeMap[route.path] = route.component }); } initComponent(Vue) { Vue.component("router-link", { props: { to: String }, render(h) { return h("a", { attrs: { href: this.to }, on: { click: this.clickhander } }, [this.$slots.default]) }, methods: { clickhander(e) { history.pushState({}, "", this.to) this.$router.data.current = this.to e.preventDefault() } } // template:"<a :href='to'><slot></slot><>" }) const self = this Vue.component("router-view", { render(h) { // self.data.current const cm = self.routeMap[self.data.current] return h(cm) } }) } initEvent() { // window.addEventListener("popstate", () => { this.data.current = window.location.pathname }) } }
任务三:模拟 Vue.js 响应式原理
-
Vue 2.x深入响应式原理 ==> Object.defineProperty():
// 模拟 Vue 中的 data 选项 let data = { msg: 'hello', count: 10 } // 模拟 Vue 的实例 let vm = {} proxyData(data) function proxyData(data) { // 遍历 data 对象的所有属性 Object.keys(data).forEach(key => { // 把 data 中的属性,转换成 vm 的 setter/setter Object.defineProperty(vm, key, { enumerable: true, configurable: true, get () { console.log('get: ', key, data[key]) return data[key] }, set (newValue) { console.log('set: ', key, newValue) if (newValue === data[key]) { return } data[key] = newValue // 数据更改,更新 DOM 的值 document.querySelector('#app').textContent = data[key] } }) }) }
-
Vue 3.x深入响应式原理(不用循环遍历,代码更简介) ==> es6的Proxy:
// 模拟 Vue 中的 data 选项 let data = { msg: 'hello', count: 0 } // 模拟 Vue 实例 let vm = new Proxy(data, { // 执行代理行为的函数 // 当访问 vm 的成员会执行 get (target, key) { console.log('get, key: ', key, target[key]) return target[key] }, // 当设置 vm 的成员会执行 set (target, key, newValue) { console.log('set, key: ', key, newValue) if (target[key] === newValue) { return } target[key] = newValue document.querySelector('#app').textContent = target[key] } })
-
发布订阅模式,由统一的调度中心调用,因此发布者和订阅者不需要知道对方的存在:
// 事件触发器 class EventEmitter { constructor () { // { 'click': [fn1, fn2], 'change': [fn] } this.subs = Object.create(null) } // 注册事件 $on (eventType, handler) { this.subs[eventType] = this.subs[eventType] || [] this.subs[eventType].push(handler) } // 触发事件 $emit (eventType) { if (this.subs[eventType]) { this.subs[eventType].forEach(handler => { handler() }) } } }
-
观察者模式,是由具体目标调度,比如当事件触发,Dep就会去调用观察者的方法,所以观察者模式的订阅者与发布者之间是存在依赖的:
// 发布者-目标 class Dep { constructor () { // 记录所有的订阅者 this.subs = [] } // 添加订阅者 addSub (sub) { if (sub && sub.update) { this.subs.push(sub) } } // 发布通知 notify () { this.subs.forEach(sub => { sub.update() }) } } // 订阅者-观察者 class Watcher { update () { console.log('update') } }
任务四:Virtual DOM 的实现原理
-
虚拟DOM,是由普通的JS对象来描述DOM对象,因为不是真实DOM对象,所以叫virtual dom。
-
为什么使用虚拟dom:
- 虚拟dom可以维护程序的状态,跟踪上一次的状态;
- 通过比较前后两次状态的差异来更新真实的dom;
-
虚拟dom的作用:
- 维护视图和状态的关系;
- 复杂视图情况下提升渲染性能;
- 除了渲染dom外,还可以实现SSR(Nuxt.js/Next.js)、原生应用(Weex/React Native)、小程序(mpvue/uni-app);
-
Virtual DOM库:Snabbdom、virtual-dom。
-
Snabbdom的使用:
import { init, h } from 'snabbdom' // 1. 导入模块 import style from 'snabbdom/modules/style' import eventlisteners from 'snabbdom/modules/eventlisteners' // 2. 注册模块 // 参数:数组,模块 // 返回值:patch函数,作用对比两个vnode的差异更新到真实DOM let patch = init([ style, eventlisteners ]) // 3. 使用 h() 函数的第二个参数传入模块需要的数据(对象) // 第一个参数:标签+选择器 // 第二个参数:如果是字符串的话就是标签中的内容 let vnode = h('div', { style: { backgroundColor: 'red' }, on: { click: eventHandler } }, [ h('h1', 'Hello Snabbdom'), h('p', '这是p标签') ]) function eventHandler () { console.log('点击我了') } let app = document.querySelector('#app') // 第一个参数:可以是DOM元素,内部会把DOM元素转换成VNode // 第二个参数:VNode // 返回值:VNde let oldVnode = patch(app, vnode) vnode = h('div', 'hello') patch(oldVnode, vnode)
-
Snabbdom的核心:
- 使用h函数创建javascript对象(VNode)描述真实dom;
- init()设置模块,创建patch();
- patch()比较新旧两个VNode;
- 把变化的内容更新到真实的Dom树上;
-
Diff 算法的执行过程:
(1) 方式为:同级别节点比较;
(2) 首先会对新老节点数组的开始和结尾节点设置标记索引,遍历的过程中移动索引,有四种情况 :
- 如果 oldStartVnode 和 newStartVnode 是 sameVnode (key 和 sel 相同) ,调用 patchVnode() 对比和更新节点,把旧开始和新开始索引往后移动 oldStartIdx++ / newStartIdx++;
- 如果 oldEndVnode和 newEndVnode 是 sameVnode (key 和 sel 相同) ,调用 patchVnode() 对比和更新节点,把旧结束和新结束索引往前移动 oldEndIdx-- / newEndIdx--;
- 如果 oldStartVnode / newEndVnode (旧开始节点 / 新结束节点) 相同,调用 patchVnode() 对比和更新节点,把 oldStartVnode 对应的 DOM 元素,移动到右边,更新索引;
- 如果 oldEndVnode / newStartVnode (旧结束节点 / 新开始节点) 相同,调用 patchVnode() 对比和更新节点,把 oldEndVnode对应的 DOM 元素,移动到左边,更新索引;
(3) 如果不是以上四种情况:
- 遍历新节点,使用 newStartNode 的 key 在老节点数组中找相同节点;
- 如果没有找到,说明 newStartNode 是新节点,创建新节点对应的 DOM 元素,插入到 DOM 树中;
- 如果找到了,判断新节点和找到的老节点的 sel 选择器是否相同,如果不相同,说明节点被修改了,重新创建对应的 DOM 元素,插入到 DOM 树中 ,如果相同,把 elmToMove 对应的 DOM 元素,移动到左边
- 当老节点的所有子节点先遍历完 (oldStartIdx > oldEndIdx),循环结束;
- 新节点的所有子节点先遍历完 (newStartIdx > newEndIdx),循环结束;
- 如果老节点的数组先遍历完(oldStartIdx > oldEndIdx),说明新节点有剩余,把剩余节点批量插入到右边
- 如果新节点的数组先遍历完(newStartIdx > newEndIdx),说明老节点有剩余,把剩余节点批量删除;
-
不带 key 的情况需要进行两次 DOM 操作,带 key 的情况只需要更新一次 DOM 操作(移动 DOM 项),所以带 key 的情况可以减少 DOM 的操作。