es6、ts、高阶js的踩坑之旅

# 大前端培训课程笔记

标签(空格分隔): es6 ts node vue react


第一阶段:JS深度剖析

模块一:ES 新特性与 JS 异步编程、TypeScript

任务一: ES新特性

  1. for循环两层嵌套作用域,for块本身一层,内部循环体一层。
  2. 最佳实践:不用var,主用const,配合let。
  3. 对象解构可设别名和默认值:{name: objName = "jack"} = obj
  4. 带标签的模板字符串:func`${name} is a ${gender}` ,作用如在func中做gender的翻译处理。
  5. ES6字符串实用扩展方法:startsWith()、endsWith()、includes()。
  6. 函数形参设默认值最好放在最后,若不是最后,调函数传参必须传入undefined才能使默认值生效。
  7. foo(first, ...args) ,..args表示剩余参数->数组,只能出现在参数列表的最后。
  8. arr2 = [...arr1] 和slice()类似,是对没有引用对象元素的数组的深拷贝,但是有的话是浅拷贝。
  9. 对象字面量增强:a.变量名属性名相同,写一遍加逗号;b.属性方法直接func()定义;c.对象动态属性用[]直接定义(计算属性名)。
  10. Object.assign(target, source1, source2); Object.is(NaN, NaN);
  11. Proxy的handler方法:(Reflect api雷同)
    541c2a0d74977a4b163cebc732472e3.png-127.2kB
  12. Proxy 对比 Object.defineProperty()的优势:a.Proxy 可以监视读写以外的操作;b.Proxy 可以很方便的监视数组操作;c.Proxy 不需要侵入对象。
  13. 对象中的键只能是字符串(ES6后还可以是Symbol()),但Map中的键可以是任意类型。
  14. Symbol()最主要的作用就是为对象添加独一无二的属性名,作为对象的私有属性。
  15. in 、Object.keys()、Json.stringfy()均会忽略对象的Symbol类型的属性。
  16. 所有可以用for...of循环的结构对象,内部都调用了一个Symbol.iterator方法。
  17. 迭代器作用:对外提供统一遍历接口(for...of),外部不用关系对象内部数据结构。
  18. 生成器:* function,用yield返回值,用.next()调用。
  19. ES2016: a.新方法:array.includes(); b.指数运算符:**。
  20. ES2017: Object.values(obj); Object.entries(obj); Object.getOwnPropertyDescriptors(); Object.defineProperties({}, descriptors) String.prototype.padStart(); String.prototype.padEnd()
    async/await

任务二:JavaScript异步编程

  1. 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);
    });
    
  2. Promise最常见的错误是嵌套使用(回调地域)。

  3. Promise链式调用:Promise对象的then()方法会返回一个全新的Promise对象,后面的then方法就是在为上一个then返回的Promise注册回调,前面then方法中回调函数的返回值会作为后面then方法返回回调的参数,如果回调中返回的是Promise,那后面then方法的回调会等待它的结束。

  4. Promise静态方法(直接返回一个Promise对象): Promise.resolve().then() / Promise.reject().catch()

  5. Promise.then 或者 .catch 的参数期望是函数,传入非函数则会发生值穿透,如下题:112ede6599390ca5a91f17fb60acc9e.png-7.3kB

  6. Promise.all()内部的全部异步执行完毕后then,Promise.race()内部的异步谁先执行完就then或者catch返回谁(用于实现处理请求超时)。

  7. 微任务会在当前线程执行完后立即执行,而宏任务要等线程和微任务都结束后才执行,目前绝大多数异步调用是宏任务,Promise是微任务。

  8. 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);
  1. 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'
})
  1. 类型断言:类型断言不是类型转换,它是用来明确某一个变量的具体类型,其方式有两种:
const num = res as number;
const num2 = <number>res // JSX下不能使用
  1. 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'
  1. 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)
  1. 类的接口与实现:
interface Eat {
  eat (food: string): void
}
class Person implements Eat{
  eat (food: string): void {
    console.log(`优雅的进餐: ${food}`)
  }
}

  1. 抽象类/方法:class/方法名前加abstract:
abstract class Animal {
  eat (food: string): void {
    console.log(`呼噜呼噜的吃: ${food}`)
  }
  abstract run (distance: number): void
}

  1. 抽象类和接口的区别:在typescript中接口和抽象类有什么区别
  2. 泛型:
// 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
}

  1. Lodash是一个著名的javascript原生库,不需要引入其他第三方依赖。是一个意在提高开发者效率,提高JS原生方法性能的JS库(另有query-string,这些之后要去项目中学习使用起来)。
  2. typescript中引入第三方模块的时候,可以:a.自己手动用declare声明模块;b.用yarn add @types/模块名 添加声明;c.第三方库有可能已经自己集成兼容ts了。

模块二:函数式编程与 JavaScript 性能优化

任务一:函数式编程范式

  1. 函数是一等公民是指,函数可以作为参数、可以作为返回值、可以赋值给变量。

  2. 高阶函数--函数作为参数(回调):

    // 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));
    
    
  3. 高阶函数--函数作为返回值(闭包):

    // 用心体会这个高阶函数抽象的例子(回调+闭包;可以用来进行并发的处理的公用方法)
    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)); // 已有缓存,不会重复执行
    
    
  4. 高阶函数总结:高阶函数就是去抽象通用的问题,实现具体的细节,封装起来,之后我们只需关注我们需要实现的目标,然后直接套用就可以。

  5. 闭包的本质:函数在执行的时候会放到一个执行栈上,当函数执行完毕后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员。

  6. 纯函数:相同的输入永远会得到相同的输出,而且没有任何可观察的副作用,例如数组的slice()是纯函数,splice()是不纯函数。

  7. 纯函数的好处:a.可缓存;b.可测试;c.并行处理。

  8. 柯里化概念:当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变),然后返回一个新的函数接收剩余的参数,返回结果;柯里化是一种对函数参数的'缓存'。

  9. 柯里化原理模拟:

    // 模拟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));
    
    
  10. 函数组合:如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数。

  11. 组合函数原理模拟:

    // 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"]));
    
    
  12. loadsh/fp模块提供了实用的对函数式编程友好的方法,参数函数优先、数据之后。

  13. 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'))
    
    
  14. 函子(Functor):是一个特殊的容器,通过一个普通的对象来实现,该对象具有map方法,map方法可以运行一个函数对值进行处理(变形关系)。可以用来在函数式编程中把副作用控制在可控的范围内、异常处理、异步操作等。

  15. 基本函子:

    // 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)
    
    
  16. 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
      }
    }
    
    
  17. 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 })
      }
    }
    
    
  18. 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())
    
    
  19. Folktale:一个标准的函数式编程库,和loadsh、remda不同,没有提供很多功能函数,而是只提供了一些函数式处理(如compose、curry)和一些函子(Task、Either、MayBe等)。

  20. 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)
        }
      })
    
    
  21. 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 性能优化

  1. 内存管理:开发者主动申请空间、使用空间、释放空间。

  2. GC算法:垃圾回收机制,常见的有:引用计数、标记清除、标记整理、分代回收。

  3. 引用计数原理:设置引用数,判断当前引用数是否为0。优点:a.发现垃圾时立即回收;b.最大限度减少程序暂停。缺点:a.无法回收循环引用的对象;b.时间开销大。

  4. 标记清除原理:遍历并标记活动对象,遍历并清除没有标记的对象。优点:可以解决循环引用不能回收的问题。缺点:回收后的空间是碎片化的,不能使空间得到最大化的使用;不会立即回收垃圾对象。

  5. 标记整理原理:标记清除的增强,标记阶段一致,清除阶段会先执行整理,移动对象位置。优点:可以解决空间碎片化问题。缺点:不会立即回收垃圾对象。

  6. V8是一个js引擎,特点是采用即时编译和内存设限。

  7. V8垃圾回收策略:分代回收、空间复制(新生代)、标记整理(新、老生代)、标记清除(老生代)、标记增量(老生代)。

  8. 界定内存问题的标准:a.内存泄漏:内存使用持续升高;b.内存膨胀:在多数设备上都存在性能问题;c.频繁的垃圾回收:通过内存变化图进行分析。

  9. 监控内存的几种方式:a.浏览器任务管理器;b.timeline时序图记录;c.堆快照查找分离dom;d.判断是否存在频繁的垃圾回收。

  10. 基于Benchmark.js的Jsperf可以进行js性能的单元测试。

  11. 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. 前端工程化主要解决的问题:

    (1) 传统语言或语法的弊端;(2) 无法使用模块化/组件化;(3) 重复的机械式工作;

    (4) 代码风格统一、质量保证;(5) 依赖后端服务接口支持;(6) 整体依赖后端项目;

  2. 工程化表现:一切以提高效率、降低成本、质量保证为目的的手段都属于工程化;

任务二:脚手架工具

  1. node的环境变量配置

    1. 在node安装路径下新建node_global、node_cache文件夹;

    2. 在终端运行:

    npm config set prefix "D:\Node\nodejs\node_global"
    npm config set cache "D:\Node\nodejs\node_cache"
    
    
    1. 在系统变量添加 NODE_PATH,值为:D:\Node\nodejs\node_global\node_modules;

    2. 在用户变量的PATH添加 D:\Node\nodejs\node_global;

    3. 重启终端。

  2. yarn的环境变量配置

    1. 在D盘新建yarn/global和yarn/cache文件夹;

    2. 在终端运行:

    yarn config set global-folder "D:\yarn\global"
    yarn config set cache-folder "D:\yarn\cache"
    
    
    1. 终端输入yarn global dir 获取yarn全局安装路径(D:\yarn\global);

    2. 在NODE_PATH中添加:D:\yarn\global;

    3. 终端输入yarn global bin 获取yarn 的 bin的 位置(D:\Node\nodejs\node_global\bin);

    4. 在系统和用户变量的PATH中添加:D:\Node\nodejs\node_global\bin;

    5. 重启终端。

  3. 脚手架的本质作用:创建项目基础结构、提供项目规范和约定(如相同的组织结构、开发范式、模块依赖、工具配置、基础代码)。

  4. Yeoman(通用型脚手架工具)的基础使用:

    // 在全局范围内安装yo
    yarn global add yo
    // 安装对应的generator
    yarn global add generator-node
    // 通过yo运行generator
    yo node
    
    
  5. yeoman使用步骤总结:

    1. 明确你的需求;
    2. 找到合适的Generator;
    3. 全局范围安装找到的Generator;
    4. 通过yo运行对应的Generator;
    5. 通过命令行交互填写选项;
    6. 生成你所需要的项目结构;
  6. Generator本质上就是一个NPM模块。

  7. Plop是一个小而美的脚手架工具,一般用于创建项目中的同类型文件,其需要使用plopfile.js定义生成规则;可以提高开发过程中的效率。

  8. 脚手架原理:

    (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)
          })
        })
      })
    })
    
    

任务三:自动化构建

  1. 自动化构建的作用:脱离运行环境兼容带来的问题,可以使得在开发阶段使用提高效率的语法、规范和标准。

  2. 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

  3. 常用的自动化构建工具:Grunt(生态完善,但基于临时文件、构建速度慢)、Gulp(基于内存、构建速度快、效率高、更方便)、FIS(百度推出,集成比较多,大而全)。

  4. Grunt的使用:

    1. yarn init --yes

    2. yarn add grunt

    3. 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)
      })
    }
    
    
    1. yarn grunt async-task
  5. Grunt标记任务失败:同步,return false;异步,done(false);一个任务失败后别的任务不会再执行。

  6. Grunt的配置方法:

    module.exports = grunt => {
      grunt.initConfig({
        foo: {
          bar: 123
        }
      })
      grunt.registerTask('foo', () => {
        console.log(grunt.config('foo.bar'))
      })
    }
    
    
  7. 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}`)
    })
    
    
  8. Grunt插件的使用:

    module.exports = grunt => {
      grunt.initConfig({
        clean: {
          temp: 'temp/**'
        }
      })
      
      grunt.loadNpmTasks('grunt-contrib-clean')
    }
    
    
  9. 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'])
    }
    
    
  10. 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()
    })
    
    
  11. 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)
    
    
  12. Gulp处理异步任务方式:回调、promise、async/await、stream。

  13. Gulp原理:读取流---->转换流---->写入流。

  14. 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'))
    }
    
    
  15. Gulp自动加载插件:gulp-load-plugins

模块二:模块化开发与规范化标准

任务一:模块化开发

  1. 模块化标准规范(最佳实践):nodejs ---> CommonJs; 浏览器环境 ---> ES Modules。

  2. ES Modules基本特性:

    • 通过给script添加type="module"就可以以ES Module的标准执行其中的JS代码;
    • ESM自动采用严格模式,忽略 'use strict'(不能全局使用this);
    • 每个ESM都是运行在单独的私有作用域中;
    • ESM是通过CORS的方式请求外部JS模块的;
    • ESM的script标签会延迟执行脚本;
  3. ES Modules导出导入: export / import。

  4. 热更新模块:browser-sync;

  5. ES Modules 导出的注意事项:

    • export / import后面的{}是固定语法,与es6字面对象和结构无关;
    • export导出的是变量的引用,不是变量的复制;
    • export导出的是变量是只读的,不可在模块外部修改;
  6. 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'
      
      
  7. ESM可以直接导出导入的成员(index.js常用作用)。

  8. browser-es-module-loader.js可以解决ESM浏览器兼容问题。

  9. Nodejs的V8.5版本后支持ESM。

  10. ESM中可以导入CommonJs模块,CommonJs始终只会导出一个默认成员,不能在CommonJs中通过require载入ESM。

  11. ESM中没有CommonJs中的那些模块全局成员了。

任务二:Webpack 打包

  1. Webpack:模块打包器、模块加载器、代码拆分、资源模块;打包工具解决的是前端整体的模块化,并不单指JS模块化。

  2. 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/'
        }
    }
    
    
  3. Webpack 资源模块加载:加载除了js外的资源模块,需要使用module加载loader,Loader是Webpack的核心特性。

    // webpack.config.js
    module: {
        rules: [
            {
                test: /.css$/,
                use: [
                    'style-loader',
                    'css-loader'
                ]
            }
        ]
    }
    
    
  4. Webpack 文件资源加载器 :

    // webpack.config.js
    const path = require('path')
    
    module.exports = {
        output: {
            publicPath: 'dist/'
        }
    }
    module: {
        rules: [
            {
                test: /.png$/,
                use: 'file-loader'
            }
        ]
    }
    
    
  5. Webpack URL 加载器 :

    {
      test: /.png$/,
      use: {
        loader: 'url-loader',
        options: {
          // 小文件使用Data URLS,减少请求次数 ---> url-loader
          // 大文件单独存放,提高加载速度 ---> file-loader
          limit: 10 * 1024 // 10 KB
        }
      }
    }
    
    
  6. Webpack 常用加载器分类 :

    • 编译转换类:css-loader、babel-loader
    • 文件操作类:file-loader
    • 代码检查类:eslint-loader
  7. webpack中的es6转换:

          {
            test: /.js$/,
            use: {
              loader: 'babel-loader',
              options: {
                presets: ['@babel/preset-env']
              }
            }
          },
    
    
  8. webpack加载资源的方式支持遵循ESM的import、CommonJs的require、AMD的define和require、css-loader中的@import和url、html代码中的img:src和a:herf。

  9. 对于同一个资源,可以依次使用多个loader,Loaders类似一个管道,最终接收的是一个javascript格式的字符串。

  10. Webpack 插件机制 :Loader专注于实现资源模块加载,而Plugin解决其他自动化工作,Plugin拥有更宽的能力范围。

  11. 常用的一些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'
        ])
      ]
    
    
  12. 自己实现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()
      ]
    }
    
    
  13. Webpack Dev Server 可以实现自动编译和自动刷新浏览器,基于内存,减少磁盘读写操作。

  14. 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
          }
        }
      },
    
    
  15. Source Map解决了源代码与运行代码不一致所产生的问题。

  16. Webpack 配置 Source Map:devtool: 'source-map'

  17. Webpack devtool 模式对比:

    • eval - 是否使用eval执行模块代码;
    • cheap - Source Map是否包含行信息;
    • module - 是否能够得到Loader处理之前的源代码;
    • 建议:开发环境--->cheap-module-eval-source-map / 生产环境--->none/nosources-source-map;
  18. Webpack HMR,模块热更新,应用运行过程中实时替换某个模块,而不会影响应用运行状态:

    devServer: {
        hot: true
        // hotOnly: true // 只使用 HMR,不会 fallback 到 live reloading
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin()
    ]
    
    
  19. Webpack HMR除了css文件外,需要手动去入口文件处理热更新逻辑:

    module.hot.accept('./better.png', () => {
        img.src = background
        console.log(background)
    })
    
    
  20. 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'])
        ]
      })
      
      
  21. webpack.DefinePlugin为代码注入全局成员:process.env.NODE_ENV。

  22. Webpack会在生产模式下自动开启Tree Shaking 来去除冗余代码,而在开发环境中,可在配置文件中使用:

    optimization: {
        // 模块只导出被使用的成员(标记无用代码)
        usedExports: true,
        // 压缩输出结果(删掉无用代码)
        minimize: true
        // 尽可能合并每一个模块到一个函数中
        concatenateModules: true
    }
    
    
  23. 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' }]
        ]
      }
    }
    
    
  24. 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())
        })
      }
      
      
  25. Webpack MiniCssExtractPlugin提取css到单个文件,Webpack OptimizeCssAssetsWebpackPlugin压缩输出的css文件。

  26. Webpack 输出文件名 Hash模式有三种:hash、chunkhash、contenthash:

    output: {
        filename: '[name]-[contenthash:8].bundle.js'
    },
    
    

任务三:其他打包工具

  1. Rollup 仅仅是一款ESM打包器,优缺点如下:

    优点:

    • 输出结果更加扁平
    • 会自动移除未引用代码
    • 打包结果依然完全可读

    缺点:

    • 加载非ESM的第三方模块比较复杂
    • 模块最终都被打包到一个函数中,无法实现HMR
    • 浏览器环境中,代码拆分功能依赖AMD库
  2. Webpack大而全,Rollup小而美,应用开发使用webpack,库、框架开发使用Rollup。

  3. 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
    
    
  4. 插件是Rollup唯一扩展途径(加载NPM模块和CommonJs模块)。

  5. Rollup代码拆分可使用动态导入方式。

  6. Parcel:零配置的前端应用打包器:

    // package.json
    "scripts": {
    	"dev": "parcel src/*.html",
    	"build": "parcel build src/*.html"
    },
    
    

任务四:模块化开发与规范化标准

  1. ESLint安装步骤:

    • 初始化项目
    • 安装ESLint模块为开发依赖
    • 通过cli命令验证安装结果
  2. eslint的配置文件示例如下:

    // .eslintrc.js
    module.exports = {
      env: {
        browser: true,
        es2020: true
      },
      extends: [
        'standard'
      ],
      parser: '@typescript-eslint/parser',
      parserOptions: {
        ecmaVersion: 11
      },
      plugins: [
        '@typescript-eslint'
      ],
      rules: {
      }
    }
    
    
  3. Stylelint用于检查CSS代码格式,有cli工具,支持sass/less/postCss,支持Gulp/Webpack:

    // .stylelintrc.js
    module.exports = {
      extends: [
        'stylelint-config-standard',
        'stylelint-config-sass-guidelines'
      ],
    }
    
    
  4. Prettier用于格式化代码: npx prettier . --write 。

  5. Husky可以实现Git Hooks的使用需求,如提交前的eslint校验,lint-staged可以实现Husky后的代码格式化,二者在package.json中进行配合和配置。

第三阶段:Vue.js框架源码与进阶

模块一:手写 Vue Router、手写响应式实现、虚拟 DOM 和 Diff 算法

任务一:Vue.js 基础回顾

  1. 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')
    
    
  2. vue生命周期,见官网。

  3. vue中的概念:

    • 插值表达式、指令、计算属性和侦听器、Class和Style绑定、条件渲染/列表渲染、表单输入绑定
    • 组件、插槽、插件、混入mixin、深入响应式原理、不同构建版本的Vue

任务二:Vue-Router 原理实现

  1. Vue Router 基础回顾-使用步骤:

    1. Vue.use(VueRouter)注册路由插件;

    2. 传入路由规则,创建router对象:

      export default new Router({
        routes: [
          {
            path: '/',
            name: 'HelloWorld',
            component: HelloWorld
          }
        ]
      })
      
      
    3. 在main.js中注册router对象:

      new Vue({
        router,
        render: h => h(App)
      }).$mount('#app')
      
      
    4. 创建路由组建的占位:

      <router-view/>
      
      
    5. 创建链接:router-link

  2. 动态路由,可以通过prop属性来接收路由参数。

  3. Vue的hash模式:

    • URL中#后面的内容作为路径地址;
    • 监听hashchange事件;
    • 根据当前路由地址找到对应的组件重新渲染;
  4. Vue的history模式:

    • 通过history.pushState()方法改变地址栏;
    • 监听popstate事件;
    • 根据当前路由地址找到对应的组件重新渲染;
  5. 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;
      }
      
      
  6. Vue的构建版本:

    • 运行版本:不支持template模板,需要打包的时候提前编译;
    • 完整版:包含运行时和编译器,体积比运行时版大10K左右,程序运行的时候把模板转换成render函数。
  7. 模拟一个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 响应式原理

  1. 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]
              }
            })
          })
        }
    
    
  2. 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]
          }
        })
    
    
  3. 发布订阅模式,由统一的调度中心调用,因此发布者和订阅者不需要知道对方的存在:

        // 事件触发器
        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()
              })
            }
          }
        }
    
    
  4. 观察者模式,是由具体目标调度,比如当事件触发,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 的实现原理

  1. 虚拟DOM,是由普通的JS对象来描述DOM对象,因为不是真实DOM对象,所以叫virtual dom。

  2. 为什么使用虚拟dom:

    • 虚拟dom可以维护程序的状态,跟踪上一次的状态;
    • 通过比较前后两次状态的差异来更新真实的dom;
  3. 虚拟dom的作用:

    • 维护视图和状态的关系;
    • 复杂视图情况下提升渲染性能;
    • 除了渲染dom外,还可以实现SSR(Nuxt.js/Next.js)、原生应用(Weex/React Native)、小程序(mpvue/uni-app);
  4. Virtual DOM库:Snabbdom、virtual-dom。

  5. 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)
    
    
  6. Snabbdom的核心:

    • 使用h函数创建javascript对象(VNode)描述真实dom;
    • init()设置模块,创建patch();
    • patch()比较新旧两个VNode;
    • 把变化的内容更新到真实的Dom树上;
  7. 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),说明老节点有剩余,把剩余节点批量删除;
  8. 不带 key 的情况需要进行两次 DOM 操作,带 key 的情况只需要更新一次 DOM 操作(移动 DOM 项),所以带 key 的情况可以减少 DOM 的操作。

模块二: Vue.js 源码分析(响应式、虚拟 DOM、模板编译和组件化)

任务一: Vue.js 源码剖析-响应式原理

posted @ 2021-03-01 09:45  fengyw  阅读(457)  评论(1编辑  收藏  举报