Fork me on GitHub
image frame

Walter

面朝大海 春暖花开

TypeScript(二):使用TypeScript改进Express路由

base : https://tasaid.com/Blog/20171011233051.html

express路由

首先我们先看一下传统的express路由是如何使用的:

import * as express from 'express';
import { Response,Request } from 'express'

const app = express();

//传统的路由方式
app.get('/',(req:Request,res:Response) => {
    res.send('首页')
})

app.get('/user',(req:Request,res:Response) => {
    res.send('我的')
})

app.listen(3000,() => {
    console.log('Example app listening at http://localhost:3000')
})

我们可以看到,传统的路由就是有点像流水线一样,而且看起来并不是那么好看。上一章我们学到了装饰器和反射的相关知识,那么现在我们需要使用这些知识来改造路由,对他进行一下升级。

改造路由

基于_装饰器和反射_,我们对路由的改造的最终结果应该像下面所示:

class User {
    @httpGet
    @path('/user/login')
    login() {
        return 'user login'
    }

    @httpGet
    @path('/user/exit')
    exit() {
        return 'user logout!!!!'
    }

}

这种方式和传统的路由使用方式相比,这种基于装饰器和反射的路由有一下优势:

  • 将路由抽离处理成来装饰器,整个router函数只需要处理业务逻辑即可
  • 隐藏res、req,路由返回值直接return即可
  • 更加优雅、配置简单明了

装饰器

这里我们需要简单实现两个装饰器:

  • httpMethods:封装http请求方法,如get、post等。用于应对不同的请求方法。
  • path:封装path,用于处理请求路径和请求参数

httpMethods

import 'reflect-metadata'

export const symbolHttpMethodsKey = Symbol("router:httpMethod")

function createMethods(method:string){
    return function httpMethodDecorator(target:any,targetKey:string){
        //注解:注入元数据 --> 请求方法
        Reflect.defineMetadata(symbolHttpMethodsKey,method,target,targetKey);
    }
}

export const httpGet = createMethods('get');
export const httpPost = createMethods('post');

path

import 'reflect-metadata'
import { Response,Request }  from 'express'

export const symbolPathKey = Symbol.for('router:path')

export const path = function(path:string):Function {
    return function(target:any,targetkey:string,descroptior:PropertyDescriptor){
        //注解:注入元数据 --> 请求路径
        Reflect.defineMetadata(symbolPathKey,path,target,targetkey);
        //如果不存在回调函数
        if(!descroptior.value) return 
        //保存原始回调函数
        let oldMethod = descroptior.value;
        //重写回调函数 
        descroptior.value = function(req:Request,res:Response){
            //获取请求参数
            const params = Object.assign({},req.body,req.query);
            //调用回调函数 获取返回值
            let result = oldMethod.call(this,params);
            //给浏览器发送结果
            res.send(result)
        }
    }
}

Router –> Controller

现在我们需要将原有的业务进行抽离,按照业务进行归类,处理成一个个的class,如:

class User {
    @httpGet
    @path('/user/login')
    login() {
        return 'user login'
    }

    @httpGet
    @path('/user/exit')
    exit() {
        return 'user logout!!!!'
    }

    /**
     * 属性装饰器
     * @param v1
     */
    // @httpGet
    // @path('/validate')
    // @validateEmptyStr
    // valid(@required v1: string) {
    //   console.log(v1)
    //   return v1
    // }
}

然后在程序入口文件处,将class里面的方法遍历一遍,将其挂载到app实例上:

export default (app: Router) => {
    let user = new User()
    for (let methodName in user) {
        let method = user[methodName]
        if (typeof method !== 'function') break
        // 得到注解的数据
        let httpMethod = Reflect.getMetadata(
            symbolHttpMethodsKey,
            user,
            methodName
        )
        let path = Reflect.getMetadata(symbolPathKey, user, methodName)

    // app.get('/', () => any)
    //在app实例挂载路由和对应的回调函数
        app[httpMethod](path, method)
    }
}

到这里,我们基于_装饰器和反射_的路由器改造就基本完成了。
下一步就是编译和运行了。

编译和运行

编译

这里我选用的是Gulp进行编译。

const { src,dest,watch,series } = require('gulp')
const del = require('del');
const ts = require('gulp-typescript');
const tsProject = ts.createProject("tsconfig.json");

function clean(cb){
    return del(['dist'],cb)
}

function build(){
    return watch('src/**/*.ts',{ events:'all',delay:500,ignoreInitial:false },function(){
        return src('src/**/*.ts')
        .pipe(tsProject())
        .pipe(dest("dist"))
    })
}

exports.default =  series(clean,build)
  • gulp-typescript 对typescript进行处理,将ts文件处理成js文件
  • 使用del在重新编译之前将上次编译的文件夹删除,类似于rm -rf

然后我们只需要在终端执行npx gulp即可。

运行

在package.json中配置scripts,执行即可。

{
    "name": "ts-router-to-constroller",
    "version": "1.0.0",
    "license": "MIT",
    "main": "dist/app.js",
    "scripts": {
        "dev": "nodemon dist/app.js"
    },
    "dependencies": {
        "@types/express": "^4.17.6",
        "@types/node": "^14.0.1",
        "del": "^5.1.0",
        "express": "^4.17.1",
        "gulp": "^4.0.2",
        "gulp-livereload": "^4.0.2",
        "gulp-nodemon": "^2.5.0",
        "gulp-typescript": "^6.0.0-alpha.1",
        "typescript": "^3.9.2"
    }
}

源码点击这里

TypeScript(一):基础

base: https://tasaid.com/Blog/20171011231943.html

前言

最近随着项目的不断迭代,团队协作之间出现了一个很大的问题:全局的数据属性和数据类型总是被篡改。而且,一些全局的公共方法在使用时,总需要看着API文档才能知道其定义:需要传入什么参数、返回什么参数。
而且,一旦其他同学修改了全局方法而没有来得及更新API文档,通知到每一位同学,那么就会造成其他调用的地方出现不可避免的bug。
在上述过程中涉及到以下问题:

  • 接口该如何描述自身的参数及返回值
  • 数据格式该如何描述
  • 在迭代中,接口参数及返回值对应的API文档该如何及时更新。

紧跟上述问题,我们就好发现在JavaScript中缺少了一样很重要的东西:静态类型。
因为有静态类型我们才知道接口需要什么参数、返回什么值、返回值是什么类型、参数是否可空等等。
在vue中使用的是 ** Flow ** 。

Flow

flow is a static type checker for javascript

flow是facebook公布的JavaScript静态类型检查器,它可以检查JavaScript中的一些bug,如:数据类型隐式转换、数据类型检查等。

// @flow
let a:number = 2;
function foo(b:tring):boolean{
    return false;
}

使用babel转换之后:

let a = 2;
function foo(b){
    return false;
}

TypeScript

相比与 ** Flow ** ,TypeScript是一门语言,它是JavaScript的超集。而且, ** Flow ** 的使用是对JavaScript的一种侵入式修改,我们想要的是一种无侵入式的。
TypeScript 的定位是做静态类型语言,而 Flow 的定位是类型检查器。

那么什么是TypeScripe呢?
TypeScript 简称 TS。TypeScript 是 JavaScript 的超集,就是在 JavaScript 上做了一层封装,封装出 TypeScript 的特性,当然最终代码可以编译为 JavaScript。
随着项目工程越来越大,越来越多的前端意识到静态数据类型的重要性,随着 TypeScript 的逐渐完善,支持者越来越多,静态数据类型的需求越来越强。
JavaScript 行至今日,灵活,动态让它活跃在编程语言界一线。而灵活,动态使得它又十分神秘,只有运行才能得到答案。类型的补充填充了 JavaScript 的缺点,从 TypeScript 编译到 JavaScript,多了静态类型检查,而又保留了 JavaScript 的灵活动态。

简单来说:动态代码一时爽,重构全家火葬场。

静态类型

这里给出一下静态类型检查和类型推导的例子:

let isDone: boolean = true
let myName: string = 'kaka'
let decLiteral: number = 10
let hexLiteral: number = 0x1000

function foo(): void {
    console.log('我没有返回值!')
}

//声明一个void类型 只能将它只为undefined null
const unusable: void = undefined
//没有类型声明 ts会进行类型推导
const myAge = 25
//等价于
const myAge2: number = 25

//联合类型
function bar(somethine: string | object): string {
    return somethine.toString()
}

//interface 定义接口
interface Person {
    name: string
    age?: number
    readonly idCard: string
    [propName: string]: any //任意属性
}

let kaka: Person = {
    name: 'kaka',
    idCard: '1234565',
    age: 25,
    firstName: 'yang',
}

//枚举
enum Days {
    Sun,
    Mon,
    Tue,
    Wed,
    Thu,
    Fri,
    Sat,
}

console.log(Days[0]);
console.log(Days['Sun']);

enum Days2 {
    Sun = 2,
    Mon = 5,
    Tue,
    Wed,
    Thu,
    Fri,
    Sat, 
} 
console.log(Days2['Sun']);
console.log(Days2['Mon']);
console.log(Days2['Tue']);

数组

let array : number[] = [1,2,3];
let array2 :Array<number> = [1,2,3,4];

//使用接口
interface Student {
    name:string,
    age:number
}

let student:Array<Student> = [
    {
        name:'kaka',
        age:0
    }
]

//可索引类型
interface Tree {
    [index:number]:string
}

let tree:Tree;

tree = ['1','2']

public 修饰的方法或属性是公有的 可以被任意访问 类的属性或方法默认为public
private 只能被父类自己访问 不能在声明它的类外部使用
protected 只能被父类和子类访问
static 只能使用类名进行访问

//抽象类 子类必须实现抽象类中的抽象方法
abstract class Person {
    public name:string = 'kaka';
    private weight:number = 25;
    protected sex:string = '男';

    abstract makeSound():void;

    move(distance:number):void {
        console.log(this.name + '已经前进了:' + distance + '步')
    }
}

class Boy extends Person {
    constructor(name,weight,sex){
        super();
        this.name = name;
        // this.weight = weight
        this.sex = sex
    }

    makeSound(){
        console.log('makeSound')
    }

}

let tom = new Boy('tom',100,25);

tom.makeSound();

tom.move(100)



abstract class Animal {
    eat(food:string):void {
        console.log('eat:' + food)
    }
    abstract sleep():void
}


class Dog extends Animal {
    public name:string;

    constructor(name:string){
        super();
        this.name = name
    }

    run(){}

    sleep(){
        console.log('Dog sleep')
    }

    private pri(){}

    protected pro(){}

    readonly legs:number = 4

    static food:string = 'bones'

}

let dog = new Dog('wangcai');

// dog.pri()

// dog.pro()

// console.log(dog.food)

console.log(Dog.food)

dog.eat('🍌')


class WorkFlow {
    step1(){
        console.log('🍌调用类step1')
        return this
    }

    step2(){
        console.log('🍎调用类step2')
        return this
    }
}

class MyFlow extends WorkFlow {
    next(){
        return this
    }
}

let myFlow = new MyFlow()

myFlow.next().step1().next().step2().step1().step2() 

接口

interface IPriceData {
    /** id */
    id: number
    /** 市场价格 */
    m: string
    /** 折扣价 */
    op: string
}


type IPriceDataArray = IPriceData[];

function getData(){
     // Promise的泛型参数使用了IPriceDataArray类型,then里面返回的数据就是IPriceDataArray类型
    return new Promise<IPriceDataArray>((resolve,reject) => {
        // fetch('https://xxxxxxx/prices/pgets?ids=P_100012&area=&source=', data => {
            // console.log(data)
            resolve([
                {
                    id:10,
                    m:'kaka',
                    op:'kaka'
                }
            ])
        // })
    })
}

getData().then(data => {
    console.log(data)
});



//高级实现

interface clockconstructor {
    new (hour:number,minute:number): Clockinstance
}

interface Clockinstance {
    tick()
}

function createClock (
    ctor: clockconstructor,
    hour: number,
    minute: number
): Clockinstance{
    return new ctor(hour,minute)
}

class DigitalClock implements Clockinstance {
    constructor(h:number,m:number) {

    }
    tick(){
        console.log("beep beep");
    }
}

class AnalogClock implements Clockinstance {
    constructor(h: number, m: number) {}
    tick() {
        console.log("tick tock");
    }
}

let digitalClock = new DigitalClock(24,59);
let analogClock = new AnalogClock(23,59);

digitalClock.tick()

接口和Type:

  • 相同点
    • 都可描述一个对象或者函数
    • 都可以扩展
  • 不同点
    • interface
      • interface 可以进行声明合并,而type不行
    • type
      • type 可以声明基本类型别名、联合类型、元组等
      • type 可以通过 typeof 获取实例类型进行赋值
//1
interface User {
    name:string,
    age:number
}

// type User = {
//     name:string,
//     age:number
// }

// interface SetUser {
//     (name:string,age:number): void
// }

type SetUser = {
    (name:string,age:number):void
}

const fn:SetUser = function(name:string,age:number){
    console.log(name,age)
}

fn('kaka',25)


//2
interface Sex {
    sex:string
}

interface BSex extends Sex {
    age:number
}

// type Sex = {
//     sex:string
// }

// type BSex = Sex & {
//     age:number
// }


//复杂的转化
// type FName = {
//   name: string;
// };
// interface FUser extends FName {
//   age: number;
// }
interface FName {
  name: string;
}
type FUser = FName & {
  age: number;
};


//不同点

//interface 能够声明合并
interface User {
  name: string
  age: number
}

interface User {
  sex: string
}

/*
User 接口为 {
  name: string
  age: number
  sex: string 
}
*/

//type 可以声明基本类型别名,联合类型,元组等类型

type UName = {
    name:string
}

type USex = {
    sex:string
}

type Boy = UName | USex;

const boy: Boy = {
    name:'l',
    sex:'girl'
}

interface Dog {
    wong();
}
interface Cat {
    miao();
}
type Pert = Dog | Cat;

type PertList = [Dog,Cat]


断言

function add(something: string | number): void {
    // 可以使用类型断言,将 something 断言成 string
    if ((<string>something).length) {
        console.log((<string>something).length)
    } else {
        console.log(something.toString().length)
    }
}

//使用as 进行推断
function sum(something: string | number): void {
    if ((something as string).length) {
        console.log((something as string).length)
    } else {
        console.log(something.toString().length)
    }
}

add('12')
sum(12)

//如果联合属性太复杂 可以给类型起个别名
// 使用 type 创建类型别名,类型别名常用于联合类型

type Name = string
type NameResolver = () => string
type NameOrAge = Name | NameResolver

function getName(name: NameOrAge): string {
    if (typeof name === 'string') {
        return name
    } else {
        return name()
    }
}


泛型

泛型就是解决类、接口、方法的复用性、以及对不确定数据类型的支持

//泛型约束
interface LengthWise {
    length:number
}

function getLength<T extends LengthWise> (arg:T):T{
    console.log("获取泛型T:",arg.length)
    return arg
}

getLength({
    length:100,
    name:'k'
})

function identity<T>(arg:T): T{
    return arg
}

let func = identity<string>('123')


//泛型类

class Girl<T> {
    public x:T;
    public y:T;
    addSum: (x:T,y:T) => T;
    // constructor(x,y){
    //     this.x = x;
    //     this.y = y
    // }
}

let girl = new Girl<number>();

girl.x = 1;
girl.y = 12;

console.log(girl.x)
console.log(girl.y)

girl.addSum = function(x:number,y:number): number{
    return x + y
}


//泛型约束中使用类型参数
function getProperty(obj:T,key:K){
    return obj[key]
}

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, "a"); // okay
getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.


// 函数重载
function getString(value:string):string {
    return value
}

function getData<T>(value:T): T {
    return value
}


getData<number>(12)
getData<string>('12')


//泛型接口

interface Teacher<T> {
    (name:T) : T
}

let MrsMa:Teacher<number> = function(x:number){
    return x
}

MrsMa(12)


interface Teacher {
    <T>(name:T) : T
}
let MrsMa:Teacher = function<T>(value:T):T {
    return value
}

MrsMa<number>(12)

装饰器

顾名思义,”装饰器” (也叫 “注解”)就是对一个 类/方法/属性/参数 的装饰。
它是对这一系列代码的增强,并且通过自身描述了被装饰的代码可能存在的行为改变。
简单来说,装饰器就是对代码的描述

/**
 * 顾名思义,"装饰器" (也叫 "注解")就是对一个 类/方法/属性/参数 的装饰。
 * 它是对这一系列代码的增强,并且通过自身描述了被装饰的代码可能存在的行为改变。
 * 简单来说,装饰器就是对代码的描述
 *
 */
import 'reflect-metadata';

var validate = function(){
    return function (
        target: any,
        targetKey: string,
        descriptor: PropertyDescriptor
    ) {
        console.log(target, targetKey, descriptor);
        //保存原来的方法
        let method = descriptor.value;
        //重写原有的方法
        descriptor.value = function(newValue:string){
            // 检查是否是空字符串
            if (!newValue) {
                throw Error('name is invalid')
            } else {
                // 否则调用原来的方法
                method.call(this, newValue)
            }
        }

    }
}

class User {
    name: string
    id: number
    constructor(name: string, id: number) {
        this.name = name
        this.id = id
    }

    // 调用装饰器
    @validate()
    changeName(newName: string) {
        this.name = newName
    }
}


let user = new User('kaka', 25)

user.changeName('');

console.log(user.name)

//类装饰器 重写类的构造函数

const A  = function(){
    return function(constructor){
        console.log(constructor);
        return class extends constructor {
            name = 'hudie'
        }
    }
}
@A()
class B {
    public name:string;
    constructor(name:string){
        this.name = name
    }
}

let b = new B('kaka');

console.log(b.name)


//方法装饰器
/**
 * target:对于类的静态成员,指的是类的构造函数。对于类的实例成员,指的是类的原型对象
 * targetKey:成员的名称
 * descriptor:成员的属性描述符。
 *  {value: any, writable: boolean, enumerable: boolean, configurable: boolean}
 */
function C(){
    return function (target:any,targetKey:string,descriptor:PropertyDescriptor){
        console.log('🍎:',target)
        console.log('🍎:',targetKey)
        console.log('🍎:',descriptor)
    }
}

class D {
    public name:string;
    constructor(name:string){
        this.name = name;
    }
    @C()
    getName(){
        return this.name
    }

    @C()
    static getAge(){
        return 10
    }
}

let d = new D('kaka');

console.log(d.getName())

D.getAge()


//访问器修饰符  同方法修饰器 只是用于访问器上
//不同点就是修饰符使用的是 访问器修饰符 
//{get: function, set: function, enumerable: boolean, configurable: boolean}
function E(){
    return function(target:any,targetKey:string,descriptor:PropertyDescriptor){
        console.log('🍎:',target)
        console.log('🍎:',targetKey)
        console.log('🍎:',descriptor)
    }
}

class F {
    private _name: string = 'kaka';
    // 装饰在访问器上
    @E()
    get name () {
        return this._name
    }
}

let f = new F();

console.log(f.name)


/**
 * 属性修饰器
 * 在运行时会被当作函数使用 传入两个参数
 *  target: 对于静态成员来说 是类的构造函数  对于实例成员来说 是类的原型对象
 *  targetKey: 成员名称
*/

function G(){
    return function(target:any,targetKey:string){
        console.log('🍎:',target)
        console.log('🍎:',targetKey)
    }
}

class H {
    //实例成员 --> 类的原型对象
    @G()
    public name:string;
    //静态属性 --> 类的构造函数
    @G()
    static money:number = 1000;

    constructor(name:string){
        this.name = name
    }
}

let h = new H('kaka');

console.log(h.name)


/**
 * 参数修饰器
 * 参数装饰器表达式会在运行时当作函数被调用,传入下列 3个参数:
 *  - target : 对于静态成员来说 指的是类的构造函数。 对于实例成员来说 指的是类的原型对象
 *  - targetKey : 成员名称
 *  - parameterIndex : 参数在函数参数中的索引
*/

function I(){
    return function(target:any,targetKey:string,parameterIndex:number){
        console.log('🍎:',target)
        console.log('🍎:',targetKey)
        console.log('🍎:',parameterIndex)
    }
}

class J {
    public name:string;
    constructor(name:string){
        this.name = name;
    }
    setName(value:string, @I() key:string){
        this.name = value;
    }
}

let j = new J('kaka');


//

import 'reflect-metadata';

const requiredKey = Symbol.for("required:key");

//定制一个参数装饰器
var required = function(){
    console.log('参数装饰器1')
    return function(target:any,targetKey:string,index:number){
        console.log('参数装饰器2')
        const rules = Reflect.getMetadata(requiredKey,target) || [];
        rules.push(index);
        // console.log(rules,'---')
        Reflect.defineMetadata(requiredKey,rules,target)
    }
}

var validateEmptyStr = function(){
    console.log('方法装饰器1')
    return function(target:any,targetKey:string,descriptor:PropertyDescriptor){
        let methods = descriptor.value;
        console.log('方法装饰器2')
        let rules = Reflect.getMetadata(requiredKey,target)
        // console.log(rules)
        descriptor.value = function (){
            let args = arguments;
            // console.log(args,'args')
            let rules = Reflect.getMetadata(requiredKey,target) as Array<number>;
            if(rules && rules.length){
                rules.forEach(key => {
                    if(!args[key]){
                       throw Error(`arguments${key} is invalid`)
                    }
                })
            }
            return methods.apply(this,arguments)
        }
    }
}

class User {
    name:string;
    id:number;

    constructor(name:string,id:number){
        this.name = name;
        this.id = id;
    }
    @validateEmptyStr()
    setName(@required() value){
        this.name = value
    }
}

let u = new User('kaka',25);
u.setName('hudie')


/**
 * 元数据反射
 * 反射就是在运行时动态获取一个对象的一切信息:属性、方法等。其特点在于动态类型反推导。
 * 在Typescript中就是在设计阶段对对象注入元数据,在运行阶段读取注入的元数据。
*/

function meta(){
    return function(target:any,targetKey:string,descriptor:PropertyDescriptor){
        //获取成员类型
        let type = Reflect.getMetadata('design:type',target,targetKey);
        console.log(type)//[Function: Function]
        //获取参数类型
        let paramsType = Reflect.getMetadata('design:paramtypes',target,targetKey);
        console.log(paramsType)//[ [Function: String] ]
        //获取返回值类型
        let returnType = Reflect.getMetadata('design:returntype',target,targetKey);
        console.log(returnType)//[Function: String]
        //获取所以元数据的key(有TypeScript注入)
        let keys = Reflect.getMetadataKeys(target,targetKey);
        console.log(keys)//[ 'design:returntype', 'design:paramtypes', 'design:type' ]
    }
}

class User {
    @meta()
    say (myName: string): string {
        return `hello, ${myName}`
    }

}

引入和编译

快速使用
//安装
yarn add typescript 
//编译
tsc index.ts

我们还可以使用ts-node进行快速运行:

//安装
yarn add ts-node -g
//执行
ts-node index.ts
tsconfig.json

tsconfig.json 是 TypeScript 的编译选项文件,通过配置它来定制 TypeScript 的编译细节。

  • 直接调用 tsc,编译器会从当前目录开始去查找 tsconfig.json 文件,逐级向上搜索父目录。
  • 调用 tsc -p,可以指定一个包含 tsconfig.json文件的目录进行编译。如果没有找到 tsconfig.json 文件,TypeScript 会编译每个文件并在对应文件的同级目录产出。

如果你要编译的是一个 Node 项目,请先安装 Node 编译依赖: npm i @types/node --save-dev,否则会出现 Node 内置模块无法找到的情况。

一个tsconfig.json的描述文件:

{
  // 编译选项
  "compilerOptions": {
    // 输出目录
    "outDir": "./output",
    // 是否包含可以用于 debug 的 sourceMap
    "sourceMap": true,
    // 以严格模式解析
    "strict": true,
    // 采用的模块系统
    "module": "esnext",
    // 如何处理模块
    "moduleResolution": "node",
    // 编译输出目标 ES 版本
    "target": "es5",
    // 允许从没有设置默认导出的模块中默认导入
    "allowSyntheticDefaultImports": true,
    // 将每个文件作为单独的模块
    "isolatedModules": false,
    // 启用装饰器
    "experimentalDecorators": true,
    // 启用设计类型元数据(用于反射)
    "emitDecoratorMetadata": true,
    // 在表达式和声明上有隐含的any类型时报错
    "noImplicitAny": false,
    // 不是函数的所有返回路径都有返回值时报错。
    "noImplicitReturns": true,
    // 从 tslib 导入外部帮助库: 比如__extends,__rest等
    "importHelpers": true,
    // 编译过程中打印文件名
    "listFiles": true,
    // 移除注释
    "removeComments": true,
    "suppressImplicitAnyIndexErrors": true,
    // 允许编译javascript文件
    "allowJs": true,
    // 解析非相对模块名的基准目录
    "baseUrl": "./",
    // 指定特殊模块的路径
    "paths": {
      "jquery": [
        "node_modules/jquery/dist/jquery"
      ]
    },
    // typescript 语法检测支持的版本库,注意不是 polyfill!只是为了有对应版本的代码特性提示!
    "lib": [
      "es2015",
      "es2015.promise"
    ]
  }
}

完整 tsconfig 配置选项的可以参考 这里,或者 tsconfig 的 json-schema

注意: TypeScript 不会做 Polyfill,例如从 es6 编译到 es5,TypeScript 编译后不会处理 es6 的那些新增的对象的方法,如果需要 polyfill 需要自行处理!
完整的编译选项请参阅 TypeScript 中文网TypeScript 官网

编译

关于编译,个人比较倾向于使用Gulp。这里给出我的个人配置。
gulpfile.js:

const { src,dest,watch,series,task } = require('gulp')
const del = require('del');
const ts = require('gulp-typescript');
const tsProject = ts.createProject("tsconfig.json");

function clean(cb){
    return del(['dist'],cb)
}

function build(){
    return watch('src/**/*.ts',{ events:'all',delay:500,ignoreInitial:false },function(){
        return src('src/**/*.ts')
        .pipe(tsProject())
        .pipe(dest("dist"))
    })
}


exports.default =  series(clean,build)

执行npx gulp即可将src下的ts文件编译到dist目录下。

Vue同构

Vue同构

本文Demo取自笔者个人博客,主要是对笔者博客系统的重构,记录心得。

什么是同构?

SPA

single page application,单页面应用。当用户第一次访问网页时,服务器会下发一个html文件,当用户再次刷新或者进行页面间跳转的时候,并不会再次请求html,而是通过刷新清除,动态切换到其他页面组件。
原理:vue-router底层会对路由变化进行监听,无论你使用的是hash模式,还是history模式:

vue-router源码:src/history/hash.js

window.addEventListener(
      supportsPushState ? 'popstate' : 'hashchange',
      () => {
        const current = this.current
        if (!ensureSlash()) {
          return
        }
        this.transitionTo(getHash(), (route) => {
          if (supportsScroll) {
            handleScroll(this.router, route, current, true)
          }
          if (!supportsPushState) {
            replaceHash(route.fullPath)
          }
        })
      }
    )

html5推出之前,vue只支持hash模式,只能通过通过onhashchange事件来监听url的变化,从而作出响应。所以我们常见的URL是这种:https://xxx.com/#/
html5推出之后,新增的pushStatereplaceStatepopstate三个API让vue彻底告别上述那种难看的URL。
下面我们来看一下这三个API在Vue的使用:

vue-router源码:src/history/html5.js

window.addEventListener('popstate', e => {
      const current = this.current
      const location = getLocation(this.base)
      if (this.current === START && location === initLocation) {
        return
      }

      this.transitionTo(location, route => {
        if (supportsScroll) {
          handleScroll(router, route, current, true)
        }
      })
 })

首先,window会监听popstate事件,在回调中获取location,然后执行transitionTo函数,我们去找transitionTo函数,可是在该文件中没有直接声明transitionTo函数,但是HTML5History却是继承了 类Hisotry,所以接下来就要在History类中寻找transitionTo函数:

vue-router源码:src/history/base.js

transitionTo (
    location: RawLocation,
    onComplete?: Function,
    onAbort?: Function
  ) {
    const route = this.router.match(location, this.current)
    this.confirmTransition(
      route,
      () => {
        //更新route
        this.updateRoute(route)
        //执行回调
        onComplete && onComplete(route)
        this.ensureURL()

        // fire ready cbs once
        if (!this.ready) {
          this.ready = true
          this.readyCbs.forEach(cb => {
            cb(route)
          })
        }
      },
      err => {
        if (onAbort) {
          onAbort(err)
        }
        if (err && !this.ready) {
          this.ready = true
          this.readyErrorCbs.forEach(cb => {
            cb(err)
          })
        }
      }
    )
  }

在上述源码中我们可以清楚的看到,transitionTo函数会首先获取匹配的路由const route = this.router.match(location, this.current),然后将route传入confirmTransition函数,执行confirmTransition函数。
在这个函数中,程序会首先判断当前路由是否和route是完全相同的,如果完全相同,那么就会取消路由更新,不做任何处理

if (isSameRoute(route, current) && route.matched.length === current.matched.length) {
  this.ensureURL()
  return abort(new NavigationDuplicated(route))
}

然后回到transitionTo函数,当执行回调的时候,首先会执行updateRoute函数

updateRoute (route: Route) {
    const prev = this.current
    this.current = route
    this.cb && this.cb(route)
    this.router.afterHooks.forEach(hook => {
      hook && hook(route, prev)
    })
  }

hook:

hook(route, current, (to: any) => {
  if (to === false || isError(to)) {
    // next(false) -> abort navigation, ensure current URL
    this.ensureURL(true)
    abort(to)
  } else if (
    typeof to === 'string' ||
    (typeof to === 'object' &&
     (typeof to.path === 'string' || typeof to.name === 'string'))
  ) {
    // next('/') or next({ path: '/' }) -> redirect
    abort()
    if (typeof to === 'object' && to.replace) {
      this.replace(to)
    } else {
      this.push(to)
    }
  } else {
    // confirm transition and pass on the value
    next(to)
  }
})

在该函数中,会对route和当前路由进行一系列的判断操作,在满足一定条件下,会执行this.replace()、 this.push()函数。

那么让我们接着去寻找这几个函数的实现:

push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(location, route => {
      pushState(cleanPath(this.base + route.fullPath))
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    }, onAbort)
  }

  replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(location, route => {
      replaceState(cleanPath(this.base + route.fullPath))
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    }, onAbort)
  }

  ensureURL (push?: boolean) {
    if (getLocation(this.base) !== this.current.fullPath) {
      const current = cleanPath(this.base + this.current.fullPath)
      push ? pushState(current) : replaceState(current)
    }
  }

vue-router:src/history/push-state.js

export function pushState (url?: string, replace?: boolean) {
  saveScrollPosition()
  // try...catch the pushState call to get around Safari
  // DOM Exception 18 where it limits to 100 pushState calls
  const history = window.history
  try {
    if (replace) {
      // preserve existing history state as it could be overriden by the user
      const stateCopy = extend({}, history.state)
      stateCopy.key = getStateKey()
      history.replaceState(stateCopy, '', url)
    } else {
      history.pushState({ key: setStateKey(genStateKey()) }, '', url)
    }
  } catch (e) {
    window.location[replace ? 'replace' : 'assign'](url)
  }
}

export function replaceState (url?: string) {
  pushState(url, true)
}

从代码中我们可以看到,其实无论是pushstate还是replacestate,其内部实现都是基于history的,都是通过history.pushState()或者history.replaceState()实现的。

综上,我们已将URL变化到Vue-router路由变化的过程理一遍,这也是vue-router的实现原理。

回到SPA的话题

SPA的页面跳转是通过vue-router监测路由变化,使用js渲染完成的。
优点:渲染快、无须向服务器请求html,减轻服务器压力
缺点:首屏加载慢、SEO差
优化方案:使用预渲染(prerender-spa-plugin)、骨架屏(vue-skeleton-webpack-pluginvue-server-renderer

MPA

multi page application 多页面应用。
每一次页面跳转的时候,后台服务器都会返回一个新的html文档,这种类型的网站也就是多页网站,也叫多页应用。
优点:SEO友好
缺点:每次跳转都需要向服务器请求html、服务器压力增大

小结:

SPA MPA
应用构成 单个HTML和各个组件组成 多个HTML页面组成
跳转方式 动态切换页面组件 一个HTML跳到另一个HTML
资源加载 切页无需加载公共资源 切页需要重新加载公共资源
URL http://xxx/shell.html#page1 http://xxx/page2.html
能否实现动画 可以 不可以
页面传参 页面传递数据容易(VuexVue中的父子组件通讯props对象) 依赖URLcookie或者localstorage,实现麻烦
SEO 需要单独的方案做处理 无需单独处理
使用范围 对体验要求高,特别是移动应用 需要对搜索引擎友好的网站
用户体验 页面片段间切换快,用户体验好,包括移动设备 页面间切换加载慢,不流畅,用户体验差,尤其在移动端

纵观SPA和MPA,各有千秋,如何在其中作出选择,着实让人为难。如果可以综合两者的优点就好了。

千呼万唤始出来 犹抱琵琶半遮面

同构应用(SSR + CSR)

终极大招来了,你准备好了吗?
Vue同构 ,综合SPA和MPA的优点,实现刷新SSR、切页CSR。
SSR:server side render
CSR:client side render

实现

这里先放上Vue SSR官方链接。

对SPA的改造

修改入口文件

在浏览器端渲染中,入口文件是main.js,而到了服务器端渲染,除了基础的main.js,还需要配置entry-client.js(客户端入口文件)和entry-server.js(服务端入口文件)。
mian.js

import Vue from "vue";
import App from "./App";
//引入router构造函数
import { createRouter } from "./router";
//引入store构造函数
import { createStore } from "./store";
// import "./assets/css/reset.css"; 去除全局样式 使用cnd 或则 在app.vue中引入
import "./utils/extend";

Vue.config.productionTip = false;

//去除传统Vue实例化方案
// new Vue({
//   el: '#app',
//   router,
//   components: { App },
//   template: '<App/>'
// })

export function createApp() {
    const router = createRouter();
    const store = createStore();
    const app = new Vue({
        router,
        store,
        render: h => h(App)
    });
    return { app, router, store };
}

在main.js中,我们使用createApp(),返回一个Vue的实例,取消传统的Vue实例化方案。主要是因为对应SSR应用,程序运行咋服务端,每个请求应该都是全新的、独立的应用程序实例,以便不会有交叉请求造成的状态污染 (cross-request state pollution)。

新增entry-client.js
import { createApp } from './main.js'

const { app,router,store } = createApp();

router.onReady(() => {
    app.$mount('#app')
})
新增entry-server.js
import { createApp } from './main';

export default context => {
    return new Promise((resolve,reject) => {
        const { app,router,store } = createApp();
        router.push(context.url);
        router.onReady(() => {
            const matchedComponents = router.getMatchedComponents();
            if(!matchedComponents){
                return reject({code:404})
            }
            return Promise.all(matchedComponents.map(component => {
                if(component.asyncData && typeof component.asyncData == 'function'){
                    return component.asyncData({
                        store
                    })
                }
            })).then(() => {
                context.state = store.state;
                resolve(app)
            }).catch(reject)
        },reject)
    })
}
router.js

给每个请求一个新的路由router实例

import Vue from "vue";
import Router from "vue-router";

Vue.use(Router);

export function createRouter() {
    return new Router({
        mode: "history",
        scrollBehavior(to, from, savedPosition) {
            if (savedPosition) {
                return savedPosition;
            } else {
                return {
                    x: 0,
                    y: 0
                };
            }
        },
        routes: [
            {
                path: "/",
                name: "index",
                component: () => import('../views/index.vue'),
                meta: { keepAlive: true }
            },
            {
                path: "/detail/:id",
                name: "detail",
                component: () => import('../views/articleDetail.vue'),
                meta: { keepAlive: false }
            }
        ]
    });
}
store.js

给每个请求一个新的store实例

import Vue from "vue";
import Vuex from "vuex";
import { post } from "../http";

Vue.use(Vuex);

function fetchData() {
    return new Promise((resolve, reject) => {
        // resolve(['bar ajax 返回数据']);
        post("/findBlogInfo", {}).then(res => {
            if (res.data.code == 200) {
                resolve(res.data.data);
            }
        });
    });
}


const isClient = (typeof window !== 'undefined') ? true : false
//如果window.__INITIAL_STATE__ 存在 即将window.__INITIAL_STATE__复制给state
const state = isClient ? (window.__INITIAL_STATE__ || {}) : {
    list: [],
    detail:{}
}
const actions = {
    getData({ commit }, payload) {
        return fetchData().then(res => {
            commit("setData", { res });
        });
    }
}
const mutations = {
    setData(state, { res }) {
        // state.list = res;
        Vue.set(state, 'list', res);
    }
}

export function createStore() {
    return new Vuex.Store({
        state,
        actions,
        mutations
    });
}
使用asyncData获取异步数据

index.vue

 asyncData({ store }) {
   return store.dispatch("getData");
 },
  computed: {
    articleList() {
      return this.$store.state.list;
    }
  },

server.js

const Koa = require("koa");
const fs = require("fs");
const serverStatic = require('koa-static');
const path = require("path");
const { createBundleRenderer, createRenderer } = require("vue-server-renderer");

const app = new Koa();
app.use(serverStatic(path.join(__dirname)+'/dist/'))
const resolve = file => path.resolve(__dirname, file);

// 生成服务端渲染函数
const renderer = createBundleRenderer(
    require("./dist/vue-ssr-server-bundle.json"),
    {
        // 模板html文件
        template: fs.readFileSync(resolve("./index.html"), "utf-8"),
        // client manifest
        clientManifest: require("./dist/vue-ssr-client-manifest.json")
    }
);

function renderToString(context) {
    return new Promise((resolve, reject) => {
        renderer.renderToString(context, (err, html) => {
            err ? reject(err) : resolve(html);
        });
    });
}

app.use(async (ctx, next) => {
    try {
        const context = {
            title: "服务端渲染测试",
            url: ctx.req.url 
        };
        ctx.status = 200
        // console.log("请求过来了:",ctx.req.url);
        const render = await renderToString(context);
        ctx.body = render;
    } catch (e) {
        console.log(e,'error');
        // 如果没找到,放过请求,继续运行后面的中间件
        next();
    }
});

app.listen(3001, () => {
    console.log("server is running at http://localhost:3001");
});

修改index.html

修改模版文件

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0,minimum=1.0,user-scalable=0">
    <meta name="description" content="个人博客" />
    <meta name="keywords" content="前端、个人博客、CSS、JAVASCRIPT、MYSQL" />
    <link href="https://cdn.bootcss.com/minireset.css/0.0.2/minireset.min.css" rel="stylesheet">
    <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css"
        integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">

    <!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
    <script defer src="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/js/bootstrap.min.js"
        integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
        crossorigin="anonymous"></script>
    <script src="https://cdn.bootcss.com/marked/0.8.2/marked.js"></script>

    <title>{{ title }}</title>
</head>

<body>
    <!--vue-ssr-outlet-->
</body>

</html>

其中最主要的就是<!--vue-ssr-outlet-->,原因后面会讲到。

修改webpack配置

webpack.base.conf.js
修改入口文件

entry: {
    //app:'./src/main.js'
    app: './src/entry-client.js'
}

webpack.prod.conf.js
服务端渲染不需要html-webpack-plugin,引入vue-server-renderer/client-plugin,生成vue-ssr-client-manifest.json,为服务器下发html时静态注入资源文件

const VueSSRClientPlugin = require("vue-server-renderer/client-plugin");
module.exports = {
    plugins: [
         // new HtmlWebpackPlugin({
    //   filename: process.env.NODE_ENV === 'testing'
    //     ? 'index.html'
    //     : config.build.index,
    //   template: 'index.html',
    //   inject: true,
    //   minify: {
    //     removeComments: true,
    //     collapseWhitespace: true,
    //     removeAttributeQuotes: true
    //     // more options:
    //     // https://github.com/kangax/html-minifier#options-quick-reference
    //   },
    //   // necessary to consistently work with multiple chunks via CommonsChunkPlugin
    //   chunksSortMode: 'dependency'
    // }),
        new VueSSRClientPlugin()
    ]
}

新增webpack.server.js

处理服务端打包

const merge = require('webpack-merge')
const nodeExternals = require('webpack-node-externals')
const baseConfig = require('./webpack.base.conf.js')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
const path = require('path');

module.exports = merge(baseConfig, {
  // 将 entry 指向应用程序的 server entry 文件
  entry: path.join(__dirname,'..','src/entry-server.js'),

  // 这允许 webpack 以 Node 适用方式(Node-appropriate fashion)处理动态导入(dynamic import),
  // 并且还会在编译 Vue 组件时,
  // 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。
  target: 'node',

  // 对 bundle renderer 提供 source map 支持
  devtool: 'source-map',

  // 此处告知 server bundle 使用 Node 风格导出模块(Node-style exports)
  output: {
    libraryTarget: 'commonjs2'
  },

  // https://webpack.js.org/configuration/externals/#function
  // https://github.com/liady/webpack-node-externals
  // 外置化应用程序依赖模块。可以使服务器构建速度更快,
  // 并生成较小的 bundle 文件。
  externals: nodeExternals({
    // 不要外置化 webpack 需要处理的依赖模块。
    // 你可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
    // 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单
    whitelist: /\.css$/
  }),

  // 这是将服务器的整个输出
  // 构建为单个 JSON 文件的插件。
  // 默认文件名为 `vue-ssr-server-bundle.json`
  plugins: [
    new VueSSRServerPlugin()
  ]
})

修改package.json

"scripts": {
    "client:prod": "scripty",
    "server:prod": "scripty",
    "build": "rm -rf dist && npm run client:prod && npm run server:prod",
    "server": "nodemon server.js "
  },

借用插件scripty优化script命令:根目录下新建script文件夹、新建子目录client、server对应客户端打包命令和服务端打包命令

client / prod.sh: node build/build.js
server / prod.sh: cross-env NODE_ENV=production webpack --config build/webpack.server.conf.js

本地打包测试

npm run build

npm run server

然后我们直接查看网页源码:
image.png
如果结果如上图所示,即证明我们的服务端渲染已经完成了。
切换页面,查看network,如果没有重新请求html,即证明已经实现切页CSR。

pm2 进程守护

因为我们的程序是运行在服务端的,为避免程序突然down掉,我们需要使用pm2进行进程守护。具体使用请参考pm2官方文档

服务器部署

将我们打包好的dist文件夹、package.json、index.html、server.js上传到我们的服务器。然后执行一下命令:

npm install

npm  run server 

配置nginx

使用nginx进行代理,实现外网访问。
nginx路径:/usr/local/nginx
配置nginx.conf

server {
    listen 80;
  server_name : xxx;//你的服务器域名 ip
  location / {
      proxy_set_header X-Real-IP  $remote_addr;
    proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
    proxy_set_header   Host   $host;
    proxy_pass  http://127.0.0.1:3001/;//对应上面server.js运行的端口
    proxy_redirect     off;
    proxy_set_header X-Nginx-Proxy true;
  }
}

优化方向

容灾

已知我们的程序运行在node中,那么我们就要做好容灾的准备,避免大流量的涌入导致node程序崩溃。
用户访问量增大,就会导致服务器压力增大,这时候我们就需要考虑降级处理,让部分用户的流量导向CSR,不再进行服务端渲染,避免服务器压力持续增大。 这一点可能需要nginx的配置。

热更新

做前端的我们都清楚,前端程序更新速度是很快的,有可能每一个月都会更新一次,而node运行在服务端的程序是不会那么频繁的更新的,如何处理node端程序和前端代码分开更新,这也是一个要优化的点。

以上两点,笔者因时间问题,暂未进行实践,后期会持续实践更新。

vue源码解读

整体架构分析

image.png
首先我们先分析一下这张图:在vue中每一个指令(包含v-if/v-show/v-model)都会被解析成一个Directive,而每一个Directive也会被相应的被一个Watcher监听,对应着一个唯一的Watcher实例。那么当数据变化的时候,如何实现数据对视图的映射呢?这里就涉及到了观察者模式:观察者模式定义了对象见一种一对多的依赖关系,当一个对象的状态发生改变的时候,所以依赖它的对象都将接收到通知,并作出更新。在vue中提出了Dep的概念用于收集依赖,每一个Watcher实例都会被收集到Dep中,当Observer观察到数据发生变化时,通知Dep数据已经发生了改变,然后Dep就会遍历它收集到的Watcher,通知Watcher数据的改变,然后Watcher将数据的改变传给Directive,由指令完成对应视图的更新。

上述分析中少了virtual dom、vnode、patch等步骤,后续会更新

接下来让我们阅读vue的源码,一步一步的分析vue的内部工作原理。

初始化

src/core/instance/index.js

我们找到vue的入口文件,从这里开始,我们逐步分析vue在初始化的时候做了什么。

import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

在上述代码中我们可以很容易的看出,我们平时所使用的new Vue就是从这里到处的一个Vue函数,在函数内部初始化state、events、liftcycle、render等,并将Vue传入。

initMixin

src/core/instance/init.js

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to avoid this being observed
    vm._isVue = true
    // 初始化组件component 合并参数
    if (options && options._isComponent) {
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    //初始化生命周期
    initLifecycle(vm)
    //初始化events
    initEvents(vm)
    //初始化render
    initRender(vm)
    //触发beforeCreate生命函数钩子
    callHook(vm, 'beforeCreate')
    //初始化injection 用于接收父组件传下的数据
    initInjections(vm) // resolve injections before data/props
    //初始化数据
    initState(vm)
    //初始化provide 用于向自组件传递数据
    initProvide(vm) // resolve provide after data/props
    //触发created生命函数钩子
    callHook(vm, 'created')

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }
        //绑定入口dom
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

从上述代码中,我们可以了解到,在initMixin的时候,vue做了一下事情:

  • 初始化组件 合并参数
  • 初始化生命周期函数
  • 初始化events
  • 初始化render函数
  • 触发beforeCreated生命函数钩子
  • 初始化injection 等待父组件数据传入
  • 初始化state
  • 初始化provide用于向子组件传数据
  • 触发craeted生命函数钩子
  • 绑定vue实例到dom

initLifecycle

初始化生命周期钩子

src/core/instance/lifecycle.js

export function initLifecycle (vm: Component) {
  const options = vm.$options

  // locate first non-abstract parent
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }

  vm.$parent = parent
  //找到跟元素
  vm.$root = parent ? parent.$root : vm

  vm.$children = []
  vm.$refs = {}

  vm._watcher = null
  vm._inactive = null
  vm._directInactive = false
  vm._isMounted = false
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}

initEvents

初始化Events

src/core/instance/initEvents.js

export function updateComponentListeners (
  vm: Component,
  listeners: Object,
  oldListeners: ?Object
) {
  target = vm
  updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
  target = undefined
}

src/core/vdom/helpers/update-listeners.js

export function updateListeners (
  on: Object,
  oldOn: Object,
  add: Function,
  remove: Function,
  createOnceHandler: Function,
  vm: Component
) {
  let name, def, cur, old, event
  for (name in on) {
    def = cur = on[name]
    old = oldOn[name]
    event = normalizeEvent(name)
    /* istanbul ignore if */
    if (__WEEX__ && isPlainObject(def)) {
      cur = def.handler
      event.params = def.params
    }
    if (isUndef(cur)) {
      process.env.NODE_ENV !== 'production' && warn(
        `Invalid handler for event "${event.name}": got ` + String(cur),
        vm
      )
    } else if (isUndef(old)) {
      if (isUndef(cur.fns)) {
        cur = on[name] = createFnInvoker(cur, vm)
      }
      if (isTrue(event.once)) {
        cur = on[name] = createOnceHandler(event.name, cur, event.capture)
      }
      add(event.name, cur, event.capture, event.passive, event.params)
    } else if (cur !== old) {
      old.fns = cur
      on[name] = old
    }
  }
  for (name in oldOn) {
    if (isUndef(on[name])) {
      event = normalizeEvent(name)
      remove(event.name, oldOn[name], event.capture)
    }
  }
}

initRender

src/core/instance/render.js

初始化render函数。

export function initRender(vm: Component) {
    //初始化根结点
  vm._vnode = null; // the root of the child tree
  vm._staticTrees = null; // v-once cached trees
  //获取初始化参数
  const options = vm.$options;
  //获取父级vNode
  const parentVnode = (vm.$vnode = options._parentVnode); // the placeholder node in parent tree
  //获取context
  const renderContext = parentVnode && parentVnode.context;
  //获取插槽
  vm.$slots = resolveSlots(options._renderChildren, renderContext);
  vm.$scopedSlots = emptyObject;
  // bind the createElement fn to this instance
  // so that we get proper render context inside it.
  // args order: tag, data, children, normalizationType, alwaysNormalize
  // internal version is used by render functions compiled from templates
  //绑定createEelment函数到vue实例
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false);
  // normalization is always applied for the public version, used in
  // user-written render functions.
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true);

  // $attrs & $listeners are exposed for easier HOC creation.
  // they need to be reactive so that HOCs using them are always updated
  //获取父组件data数据
  const parentData = parentVnode && parentVnode.data;

  /* istanbul ignore else */
  //监听vm实例上的$attrs $listeners属性
  if (process.env.NODE_ENV !== "production") {
     defineReactive( vm,  "$attrs", (parentData && parentData.attrs) || emptyObject, () => {
        !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm);
      },
      true
    );
    defineReactive( vm, "$listeners", options._parentListeners || emptyObject, () => {
        !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm);
      },
      true
    );
  } else {
    defineReactive( vm,  "$attrs", (parentData && parentData.attrs) || emptyObject, null,true
    );
    defineReactive( vm, "$listeners", options._parentListeners || emptyObject, null,true
    );
  }
}

callHook

src/core/instance/lifecycle.js

触发各个生命函数钩子。

export function callHook (vm: Component, hook: string) {
  // #7573 disable dep collection when invoking lifecycle hooks
  //收集依赖 入栈
  pushTarget()
  const handlers = vm.$options[hook]
  const info = `${hook} hook`
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      invokeWithErrorHandling(handlers[i], vm, null, vm, info)
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
  //出栈
  popTarget()
}

initInjections

src/core/instance/inject.js

export function initInjections (vm: Component) {
  //获取vm上所有的inject
  const result = resolveInject(vm.$options.inject, vm)
  if (result) {
    //避免调用defineReactive时触发依赖收集
    toggleObserving(false)
    Object.keys(result).forEach(key => {
        defineReactive(vm, key, result[key])
    })
    toggleObserving(true)
  }
}

initState

src/core/instance/state.js

//vue初始化state
export function initState(vm: Component) {
  vm._watchers = [];
  //获取参数options
  const opts = vm.$options;
  //如果存在props 则初始化initProps
  if (opts.props) initProps(vm, opts.props);
  //如果存在methods 则初始化initMethods
  if (opts.methods) initMethods(vm, opts.methods);
  //如果存在data 则initData(vm)
  if (opts.data) {
    initData(vm);
  } else {
    observe((vm._data = {}), true /* asRootData */);
  }
  if (opts.computed) initComputed(vm, opts.computed);
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch);
  }
}

function initProps(vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {};
  const props = (vm._props = {});
  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  const keys = (vm.$options._propKeys = []);
  //判断是否是根结点
  const isRoot = !vm.$parent;
  // root instance props should be converted
  //修改observer/index.js中shouldObserve变量  将根结点的数据转化成响应式的
  if (!isRoot) {
    toggleObserving(false);
  }
  for (const key in propsOptions) {
    keys.push(key);
    const value = validateProp(key, propsOptions, propsData, vm);
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== "production") {
      const hyphenatedKey = hyphenate(key);
      if (
        isReservedAttribute(hyphenatedKey) ||
        config.isReservedAttr(hyphenatedKey)
      ) {
        warn(
          `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
          vm
        );
      }
      defineReactive(props, key, value, () => {
        if (!isRoot && !isUpdatingChildComponent) {
          warn(
            `Avoid mutating a prop directly since the value will be ` +
              `overwritten whenever the parent component re-renders. ` +
              `Instead, use a data or computed property based on the prop's ` +
              `value. Prop being mutated: "${key}"`,
            vm
          );
        }
      });
    } else {
      //将props变成响应式的数据 监听每一个props的改变
      defineReactive(props, key, value);
    }
    // static props are already proxied on the component's prototype
    // during Vue.extend(). We only need to proxy props defined at
    // instantiation here.
    if (!(key in vm)) {
      proxy(vm, `_props`, key);
    }
  }
  //最后将observer/index.js中shouldObserve变量 变为true
  toggleObserving(true);
}

function initData(vm: Component) {
  let data = vm.$options.data;
  //如果data是function类型 则使用getData获取数据 否则为{}
  data = vm._data = typeof data === "function" ? getData(data, vm) : data || {};
  if (!isPlainObject(data)) {
    data = {};
  // proxy data on instance
  const keys = Object.keys(data);
  //获取当前vue实例中的props methods 检查是否和data中的属性有重复的
  const props = vm.$options.props;
  const methods = vm.$options.methods;
  let i = keys.length;
  while (i--) {
    const key = keys[i];
    proxy(vm, `_data`, key);
  }
  // observe data 将data转化成响应式的
  observe(data, true /* asRootData */);
}

function initMethods(vm: Component, methods: Object) {
  const props = vm.$options.props;
  for (const key in methods) {
    /**
     * 如果methods中的method不为funciton 则使用一个空函数替代
     * export function noop (a?: any, b?: any, c?: any) {}
     * 
     * 否则使用bind绑定this
     * export const bind = Function.prototype.bind
        ? nativeBind
        : polyfillBind
     */
    vm[key] =
      typeof methods[key] !== "function" ? noop : bind(methods[key], vm);
  }
}

function initComputed(vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = (vm._computedWatchers = Object.create(null));
  // computed properties are just getters during SSR
  const isSSR = isServerRendering();

  for (const key in computed) {
    const userDef = computed[key];
    const getter = typeof userDef === "function" ? userDef : userDef.get;

    if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      );
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if (!(key in vm)) {
      defineComputed(vm, key, userDef);
    }
  }
}
//初始化watch 遍历watch对象 将其每一个属性都调用createWatcher变成响应式的
function initWatch(vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key];
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i]);
      }
    } else {
      createWatcher(vm, key, handler);
    }
  }
}
//创建watcher 返回一个响应式对象
function createWatcher(
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  if (isPlainObject(handler)) {
    options = handler;
    handler = handler.handler;
  }
  if (typeof handler === "string") {
    handler = vm[handler];
  }
  return vm.$watch(expOrFn, handler, options);
}

provide

src/core/instance/provide.js

初始化provide 并将其绑定在vue实例上

export function initProvide (vm: Component) {
  const provide = vm.$options.provide
  if (provide) {
    vm._provided = typeof provide === 'function'
      ? provide.call(vm)
      : provide
  }
}

stateMixin

export function stateMixin(Vue: Class<Component>) {
  // flow somehow has problems with directly declared definition object
  // when using Object.defineProperty, so we have to procedurally build up
  // the object here.
  const dataDef = {};
  dataDef.get = function () {
    return this._data;
  };
  const propsDef = {};
  propsDef.get = function () {
    return this._props;
  };
  Object.defineProperty(Vue.prototype, "$data", dataDef);
  Object.defineProperty(Vue.prototype, "$props", propsDef);
  //挂载$set API
  Vue.prototype.$set = set;
  //挂载$delete API
  Vue.prototype.$delete = del;
  //实现$watch方法
  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this;
    //如果是对象 则监听对象
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options);
    }
    options = options || {};
    options.user = true;
    const watcher = new Watcher(vm, expOrFn, cb, options);
    //如果设置了immediate 则立即执行一次
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value);
      } catch (error) {
        handleError(
          error,
          vm,
          `callback for immediate watcher "${watcher.expression}"`
        );
      }
    }
    //返回一个取消监听的函数unwatchFn
    return function unwatchFn() {
      watcher.teardown();
    };
  };
}

在stateMixin中,vue做了这些事:

  • 重写了Vue原型上$data 和 $props的get方法。
  • 在Vue原型上挂载$set、$delete、$watch方法。

vm.$watch的实现已经在上述代码中,我们看看vm.$set和vm.$delete的实现

src/core/observer/index.js

/**
 * Set a property on an object. Adds the new property and
 * triggers change notification if the property doesn't
 * already exist.
 * 新增响应式数据
 */
export function set(target: Array<any> | Object, key: any, val: any): any {
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key);
    //调用splice 新增一个元素 并触发数组的setter将其转成响应式数据
    target.splice(key, 1, val);
    return val;
  }
  if (key in target && !(key in Object.prototype)) {
    target[key] = val;
    return val;
  }
  //获取observer实例
  const ob = (target: any).__ob__;
  if (target._isVue || (ob && ob.vmCount)) {
    return val;
  }
  //如果不存在__ob__ 则该对象不是响应式的 直接操作属性即可
  if (!ob) {
    target[key] = val;
    return val;
  }
  defineReactive(ob.value, key, val);
  //通知watcher数据的变动
  ob.dep.notify();
  return val;
}

/**
 * Delete a property and trigger change if necessary.
 * 调用splice删除元素 整体流程同set
 */
export function del(target: Array<any> | Object, key: any) {

  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1);
    return;
  }
  const ob = (target: any).__ob__;

  if (!hasOwn(target, key)) {
    return;
  }
  delete target[key];
  if (!ob) {
    return;
  }
  ob.dep.notify();
}

eventsMixin

src/core/instance/event.js

export function eventsMixin (Vue: Class<Component>) {
  const hookRE = /^hook:/
   //在vue原型上挂载$on方法
  Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
    const vm: Component = this
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$on(event[i], fn)
      }
    } else {
      (vm._events[event] || (vm._events[event] = [])).push(fn)
      // optimize hook:event cost by using a boolean flag marked at registration
      // instead of a hash lookup
      if (hookRE.test(event)) {
        vm._hasHookEvent = true
      }
    }
    return vm
  }
  //在vue原型上挂载$once方法
  Vue.prototype.$once = function (event: string, fn: Function): Component {
    const vm: Component = this
    function on () {
      vm.$off(event, on)
      fn.apply(vm, arguments)
    }
    on.fn = fn
    vm.$on(event, on)
    return vm
  }
  //在vue原型上挂载$off方法
  Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
    const vm: Component = this
    // all
    if (!arguments.length) {
      vm._events = Object.create(null)
      return vm
    }
    // array of events
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$off(event[i], fn)
      }
      return vm
    }
    // specific event 获取所有对应的事件
    const cbs = vm._events[event]
    if (!cbs) {
      return vm
    }
    if (!fn) {
      vm._events[event] = null
      return vm
    }
    // specific handler
    let cb
    let i = cbs.length
    while (i--) {
      cb = cbs[i]
      if (cb === fn || cb.fn === fn) {
        cbs.splice(i, 1)
        break
      }
    }
    return vm
  }
  //在vue原型上挂载$emit方法
  Vue.prototype.$emit = function (event: string): Component {
    const vm: Component = this;
    let cbs = vm._events[event]
    if (cbs) {
      cbs = cbs.length > 1 ? toArray(cbs) : cbs
      const args = toArray(arguments, 1)
      const info = `event handler for "${event}"`
      for (let i = 0, l = cbs.length; i < l; i++) {
        invokeWithErrorHandling(cbs[i], vm, args, vm, info)
      }
    }
    return vm
  }
}

在eventsMixin中,使用了观察者模式,实现在vue挂载了$on、$once、$off、 $emit的方法。

  • $on:订阅某个事件
  • $once:订阅某个事件 回调只会触发一次
  • $off:取消订阅的 某个事件
  • $emit:发布某个事件

lifecycleMixin

src/core/instance/lifecycle.js

export function lifecycleMixin (Vue: Class<Component>) {
  //在vue原型上挂载私有方法_update
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el//真实dom
    const prevVnode = vm._vnode//虚拟dom
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    if (!prevVnode) {
      // initial render
      //如果prevVnode不存在 执行新增操作
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      //否则 执行dom diff
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    restoreActiveInstance()
    // update __vue__ reference
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook.
  }
  //在vue原型上挂载公共方法$forceUpdate
  Vue.prototype.$forceUpdate = function () {
    const vm: Component = this
    //调用私有方法update触发更新操作
    if (vm._watcher) {
      vm._watcher.update()
    }
  }
  //在vue原型上挂载公共方法$destory
   Vue.prototype.$destroy = function () {
    const vm: Component = this
    if (vm._isBeingDestroyed) {
      return
    }
    //触发boforeDestory生命函数钩子
    callHook(vm, 'beforeDestroy')
    vm._isBeingDestroyed = true
    // remove self from parent
    const parent = vm.$parent
    //清除当前组件和父组件之间的关联 将当前组件从父组件中移除
    //如果有父组件 且父组件没有执行销毁操作 且不是抽象组件
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      remove(parent.$children, vm)
    }
    //去除组件上所有的watcher

    // teardown watchers
    //去除组件自带的watch
    if (vm._watcher) {
      vm._watcher.teardown()
    }
    //去除用户主动使用vm.$watch实现的监听
    let i = vm._watchers.length
    while (i--) {
      vm._watchers[i].teardown()
    }
    // remove reference from data ob
    // frozen object may not have observer.
    if (vm._data.__ob__) {
      vm._data.__ob__.vmCount--
    }
    // call the last hook...
    //如果为true 则组件正在执行销毁
    vm._isDestroyed = true
    // invoke destroy hooks on current rendered tree
    //解除模版中所有的指令
    vm.__patch__(vm._vnode, null)
    // fire destroyed hook
    //触发destrotyed
    callHook(vm, 'destroyed')
    // turn off all instance listeners.
    //移除所有的事件监听
    vm.$off()
    // remove __vue__ reference
    if (vm.$el) {
      vm.$el.__vue__ = null
    }
    // release circular reference (#6759)
    if (vm.$vnode) {
      vm.$vnode.parent = null
    }
  }
}

在lifecycleMixin中,vue做了这些事:

  • 在vue原型上挂载了私有方法_update
  • 挂载了公共方法$forceUpdate方法
  • 挂载了公共方法$destroy方法

renderMixin

src/core/instance/render.js

export function renderMixin(Vue: Class<Component>) {
  //install runtime convenience helpers
  //在vue原型上挂载一下方法和属性
  installRenderHelpers(Vue.prototype);
  //在vue原型上挂载$nextTick方法 
  Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this);
  };
  //在vue原型挂载私有方法_render
  Vue.prototype._render = function (): VNode {
    const vm: Component = this;
    const { render, _parentVnode } = vm.$options;

    if (_parentVnode) {
      vm.$scopedSlots = normalizeScopedSlots(
        _parentVnode.data.scopedSlots,
        vm.$slots,
        vm.$scopedSlots
      );
    }

    // set parent vnode. this allows render functions to have access
    // to the data on the placeholder node.
    vm.$vnode = _parentVnode;
    // render self
    let vnode;
    try {
      // There's no need to maintain a stack because all render fns are called
      // separately from one another. Nested component's render fns are called
      // when parent component is patched.
      currentRenderingInstance = vm;
      vnode = render.call(vm._renderProxy, vm.$createElement);
    } catch (e) {
      handleError(e, vm, `render`);
      // return error render result,
      // or previous vnode to prevent render error causing blank component
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== "production" && vm.$options.renderError) {
        try {
          vnode = vm.$options.renderError.call(
            vm._renderProxy,
            vm.$createElement,
            e
          );
        } catch (e) {
          handleError(e, vm, `renderError`);
          vnode = vm._vnode;
        }
      } else {
        vnode = vm._vnode;
      }
    } finally {
      currentRenderingInstance = null;
    }
    // if the returned array contains only a single node, allow it
    if (Array.isArray(vnode) && vnode.length === 1) {
      vnode = vnode[0];
    }
    // return empty vnode in case the render function errored out
    if (!(vnode instanceof VNode)) {
      if (process.env.NODE_ENV !== "production" && Array.isArray(vnode)) {
        warn(
          "Multiple root nodes returned from render function. Render function " +
            "should return a single root node.",
          vm
        );
      }
      vnode = createEmptyVNode();
    }
    // set parent
    vnode.parent = _parentVnode;
    return vnode;
  };
}

在renderMixin中,vue做了这些事:

  • 通过函数installRenderHelpers,在vue原型上挂载了一些方法和属性
  • 挂载了公共方法$nextTick方法
  • 挂载了私有方法_render方法

nextTick

src/core/util/next-tick.js

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'

export let isUsingMicroTask = false

const callbacks = []
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 timerFunc

// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
    const p = Promise.resolve()
    timerFunc = () => {
        p.then(flushCallbacks)
        // In problematic UIWebViews, Promise.then doesn't completely break, but
        // it can get stuck in a weird state where callbacks are pushed into the
        // microtask queue but the queue isn't being flushed, until the browser
        // needs to do some other work, e.g. handle a timer. Therefore we can
        // "force" the microtask queue to be flushed by adding an empty timer.
        if (isIOS) setTimeout(noop)
    }
    isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
    isNative(MutationObserver) ||
    // PhantomJS and iOS 7.x
    MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
    // Use MutationObserver where native Promise is not available,
    // e.g. PhantomJS, iOS7, Android 4.4
    // (#6466 MutationObserver is unreliable in IE11)
    let counter = 1
    //MutationObserver 监听指定节点的变更 如果有变化 则执行回调
    const observer = new MutationObserver(flushCallbacks)
    const textNode = document.createTextNode(String(counter))
    observer.observe(textNode, {
        characterData: true
    })
    timerFunc = () => {
        counter = (counter + 1) % 2
        textNode.data = String(counter)
    }
    isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
    // Fallback to setImmediate.
    // Technically it leverages the (macro) task queue,
    // but it is still a better choice than setTimeout.
    timerFunc = () => {
        setImmediate(flushCallbacks)
    }
} else {
    // Fallback to setTimeout.
    timerFunc = () => {
        setTimeout(flushCallbacks, 0)
    }
}

export function nextTick(cb?: Function, ctx?: Object) {
    let _resolve
    callbacks.push(() => {
        if (cb) {
            try {
                cb.call(ctx)
            } catch (e) {
                handleError(e, ctx, 'nextTick')
            }
        } else if (_resolve) {
            _resolve(ctx)
        }
    })
  //如果当前执行队列中无任务正在执行 立即执行
    if (!pending) {
        pending = true
        timerFunc()
    }
    // $flow-disable-line
    if (!cb && typeof Promise !== 'undefined') {
        return new Promise(resolve => {
            _resolve = resolve
        })
    }
}

原理:
nextTick接收两个参数:第一个参数是回调函数、第二个参数是上下文对象ctx。
作用:将回调函数延迟到下次Dom更新之后执行。
实现:通过判断执行环境是否支持以下属性:Promise、MutationObserver、setImmdiate 、setTimeout。然后将回调函数放进上述所说的四个宏任务或者微任务的回调中执行。

小结

在上面,我们逐步分离、了解了Vue的初始化、内部实现原理,理清了每一步的实现步骤。总的来说,Vue初始化的时候,做了以下事情:

  1. 初始化生命周期、events、render、触发beforeCreate、初始化injection、state、provide、触发created
  2. 在vue原型对象傻姑娘绑定了一些属性和方法,如
    1. 与数据相关的:$set 、$delete 、$watch
    2. 与事件相关的:$on 、$once 、$off 、$emit
    3. 与生命周期相关的:$foreUpdate、 $destroy、 $nextTick、 $mount
  3. _

那么接下来我们需要了解的就是Vue在运行时如何处理数据更新的。

运行时

Object.definePrototype

vue内部使用Object.definePrototype对对象进行监测,举个🌰:

function reactive(target,key,value){
    return Object.defineProperty(target,key,{
        enumerable:true,
        configurable:true,
        get:function(){
            console.log(`获取${key}的值:`,value)
            return value
        },
        set:function(newValue){
            console.log(`设置${key}的值:`,newValue);
            value = newValue
        }
    })
}

function Observer(data){
    Object.keys(data).forEach(key => {
        reactive(data,key,data[key])
    })
}

let obj = {
    a:10,
    b:20
}


Observer(obj)

obj.a;//获取a的值: 10
obj.a = 30;//设置a的值: 30

从上述代码中我们可以看到,我们可以使用循环遍历的方式,将其中每一个属性都进行监测,这样之后对对象的读取操作都会被监测到。
那么Array数组也是对象,为什么vue源码中会对数组的方法进行重写呢?下面我们来讨论下数组的情况。

let arr = [1,2,3];

Observer(arr);

arr[0]

arr.unshift(0)

image.png
我们可以看到当我们对数组进行操作的时候,是可以监控到数组的变动的。但是,为什么会打印多次数组变动呢?这里就涉及到一点数据结构的知识了,在内存中,数组是存储在堆中的,而且是连续的,如:

…其他内容 1 2 3 4 5 6 …其他内容

那么当我们在操作数组(插入、删除)的时候,就会改变数组的长度,就会移动整个数组,如:arr[i+1] = arr[i],移动整个数组就会逐步触发我们提前定义的setter/getter,所以就会打印多次。

但是这样并不是我们想要达到的结果,我们不希望它多次触发setter/getter。所以,在Vue源码实现中,拦截重写了这些可以改变数组的方法:push、pop、shift、unshift、sort、reverse、splice
具体实现在:src/core/observer/array.js

对于数组类型的数据,由于JavaScript的限制,Vue不能监测到内部的变化,会重写数组的部分方法,重写之后可以达到以下两个目的:

  • 当数组发生变化之后触发notify,通知相关依赖watcher
  • 对于数组新增元素,使用observe进行监听,将新元素也变成响应式的

题外话:Vue2之所以不支持IE8以下,就是因为内部使用了这个API。Vue3使用了Proxy之后,彻底不支持IE了😊。百度之前出的框架SanJS内部使用了defineSetterdefineGetter_ 可以在IE中运行。

Observer

观察者模式是软件设计模式的一种。在此种模式中,一个目标对象管理所有相依于它的观察者对象,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。此种模式通常被用来实时事件处理系统。

订阅者模式涉及三个对象:发布者、主题对象、订阅者,三个对象间的是一对多的关系,
每当主题对象状态发生改变时,其相关依赖对象都会得到通知,并被自动更新。
在Vue中Observer会观察数组和对象两种数据类型:

src/core/observer/index.js src/core/observer/array.js

对于数组类型的数据,由于JavaScript的限制,Vue不能监测到内部的变化,会重写数组的部分方法,重写之后可以达到以下两个目的:

  • 当数组发生变化之后触发notify,通知相关依赖watcher
  • 对于数组新增元素,使用observe进行监听,将新元素也变成响应式的

而对于Object类型的数据,则遍历它的每个key,使用 defineProperty 设置 getter
setter,当触发getter的时候,observe开始收集依赖,触发setter的时候,observe触发notify

Observer 对象的标志就是
__ob__ 这个属性,这个属性保存了 Observer 对象自己本身。
对象在转化为 Observer 对象的
过程中是一个递归的过程,对象的子元素如果是对象或数组的话,也会转化为 Observer 对象。

其实 observeArray 方法就是对数组进行遍历,递归调用
observe 方法,最终都会走入
walk 方监控单个元素。而
walk 方法就是遍历对象,结合
defineReactive 方法递归将属
性转化为 gettersetter

export function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  //实例话Dep
  const dep = new Dep();

  const property = Object.getOwnPropertyDescriptor(obj, key);
  //如果对象不可更改
  if (property && property.configurable === false) {
    return;
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get;
  const setter = property && property.set;
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key];
  }

  let childOb = !shallow && observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val;
      if (Dep.target) {
        //收集依赖watcher
        dep.depend();
        if (childOb) {
          childOb.dep.depend();
          //如果是数组,循环遍历
          if (Array.isArray(value)) {
            dependArray(value);
          }
        }
      }
      return value;
    },
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val;
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return;
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== "production" && customSetter) {
        customSetter();
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return;
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = !shallow && observe(newVal);
      //通知watcher数据变更
      dep.notify();
    },
  });
}
export function observe(value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return;
  }
  let ob: Observer | void;
  //避免重复监听 observer挂载到响应式数据的__ob__属性上
  if (hasOwn(value, "__ob__") && value.__ob__ instanceof Observer) {
    ob = value.__ob__;
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value);
  }
  if (asRootData && ob) {
    ob.vmCount++;
  }
  return ob;
}
/**
 * Collect dependencies on array elements when the array is touched, since
 * we cannot intercept array element access like property getters.
 * 遍历数组 将每一个元素变成响应式的
 */
function dependArray(value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i];
    e && e.__ob__ && e.__ob__.dep.depend();
    if (Array.isArray(e)) {
      dependArray(e);
    }
  }
}
/**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   * 遍历对象 监测每一个属性
   */
  walk(obj: Object) {
    const keys = Object.keys(obj);
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i]);
    }
  }

Watcher

src/core/observer/watcher.js

Watcher是将模版和Observer链接到一起的纽带。Watcher在发布订阅模式中的订阅者。Watcher接收的参数中:expOrFn: String | Function , cb: Function。其中:expOrFn如果是一个函数,则直接赋值给this.getter,用于指定当前订阅者获取数据的方法。如果是字符串,则调用parsePath函数将其转化为一个可执行函数。cb是数据更新时的回调函数。

export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor(
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm;
    if (isRenderWatcher) {
      vm._watcher = this;
    }
    //将当前的Watcher类推送到Vue实例
    vm._watchers.push(this);
    // parse expression for getter
    if (typeof expOrFn === "function") {
      //如果为函数,则相当于指定类订阅者获取数据的方法 每次订阅者通过getter获取到数据之后,与之前的数据进行对比
      this.getter = expOrFn;
    } else {
      //否则将表达式解析为可执行函数
      this.getter = parsePath(expOrFn);
      if (!this.getter) {
        this.getter = noop;
      }
    }
    //如果lazy不为true 则立即搜集依赖
    this.value = this.lazy ? undefined : this.get();
  },
    /**
   * Evaluate the getter, and re-collect dependencies.
   * 收集依赖
   */
  get() {
    pushTarget(this);
    let value;
    const vm = this.vm;
    try {
      //调用getter 收集依赖
      value = this.getter.call(vm, vm);
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`);
      } else {
        throw e;
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value);
      }
      popTarget();
      this.cleanupDeps();
    }
    return value;
  }/**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update() {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true;
    } else if (this.sync) {
      //如果是同步
      this.run();
    } else {
      queueWatcher(this);
    }
  }

依赖收集的入口就是get函数。getter函数是用来连接监控属性和Watcher的关键。只有通过Watchergetter才会收集依赖。而所谓的搜集的依赖就是当前Watcher实例初始化时传入expOrFn中的每一项数据,然后触发该数据的getter函数,而getter函数通过依赖的Dep.target是否存在来判断是否是初始化调用还是正常的数据读取。如果有target,则进行依赖收集(Observer/index.js)。

我们注意看上面代码中的update方法,该方法会判断是否是同步执行this.sync,如果是,则立即执行;否则,调用queueWatcher方法,将当前watcher放进队列中。
我们来看queueWatcher的实现:

src/core/observe/scheduler.js

/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id;
  //判断queuewatcher中是否已存在该watcher
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
        //如果!flushing 则将其push进队列
      queue.push(watcher)
    } else {
        //否则将其插入到queue中
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    //如果没有等待
    if (!waiting) {
      //改变状态 waiting
      waiting = true
      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}

Dep

src/core/observer/dep.js

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 * 收集依赖
 */
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++;
    //收集依赖watcher
    this.subs = []
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  //删除依赖watcher
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
  //添加依赖watcher
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
  //数据更新 通知相关依赖更新
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    //如果不是异步 则将其进行排序
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

当数据更新时,触发setter,会调用notify方法通知触发订阅者(依赖watcher)的update方法,通知订阅者更新视图。

Directive

image.png

当一个组件初始化的时候,会进行数据的初始化和模版编译。初始化数据的时候,会将页面中的数据进行Observer,调用Object.defineProrotype,将数据转化为setter/getter(响应式的数据)。与此同时,模版编译的时候也会将对应的指令转化为Directive,每一个Directive对应一个watcher,然后触发getterwatcher收集进Dep中。当数据发生变化的时候,触发setter,对应的Dep会遍历每一个依赖,通知watcher数据已发生了变化,然后watcher调用update方法触发Directiveupdate更新视图(中间暂时省略vnodedom diffpatch等过程)。

关于编译这块vue分了两种类型,一种是文本节点,一种是元素节点。

image.png
vue内置了这么多的指令, 这些指令都会抛出两个接口 bindupdate,这两个接口 的作用是,编译的最后一步是执行所有用到的指令的bind方 法,而 update 方法则是当 watcher 触发 update 时, Directive会触发指令的update 方法
observe -> 触发setter -> watcher -> 触发update -> Directive -> 触发update -> 指令

this._directive.push(
    new Directive(descriptor,this,node,host,scope,frag)
)
  1. 所有 tagtrue 的数据中的扩展对象拿出来生成一个 Directive 实例并添加到 _directives 中( _directives 是当前 vm 中存储所有 directive 实例的地方)。
  2. 调用所有已绑定的指令的 bind 方法
  3. 实例化一个 Watcher,将指令的 updatewatcher 绑定在一起(这样就实现了 watcher 接收到消息后触发的 update 方法,指令可以做出对应的更新视图操作)
  4. 调用指令的 update,首次初始化视图
  5. 这里有一个点需要注意一下,实例化 Watcher 的时候,Watcher 会将自己主动的推入 Dep 依赖中

Scheduler.js

src/core/observer/scheduler.js

当数据发生变更之后,触发 setter,然后在Dep中会遍历当前数据的 Watcher,执行 Watcher.update( )方法,这里我们看下 Watcher.update()的代码:

/**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update() {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true;
    } else if (this.sync) {
      //如果是同步 直接执行
      this.run();
    } else {
      //否则放进执行队列中
      queueWatcher(this);
    }
  }

我们可以看到,如果是同步的,则立即执行回调函数,否则,将当前 Watcher 实例放进 watcher queue 中等待执行。

/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id;
  //判断queuewatcher中是否已存在该watcher
  if (has[id] == null) {
    has[id] = true
     if (!flushing) {
      //如果队列中无watcher
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      //否则根据优先级,将其插入到queue中
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    //如果没有等待
    if (!waiting) {
      waiting = true
      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}

queueWatcher 函数中,vue 会将 watcher 放入watcher queue,并且,如果当前没有任务在执行的时候,则调用 nextTick 执行当前watcher.run,即执行回调函数。
nextTick 函数的源码中,为了让 flush 动作能在当前 Task 结束后尽可能早的开始,Vue 会优先尝试将任务micro-task 队列,具体来说, 在浏览器环境中 Vue 会优先尝试使用 MutationObserver APIPromise,如果两者都不可用,则 fallbacksetTimeout

Keep-alive

src/core/components/keep-alive.js

或许大家都接过这样一个需求,当用户在A列表页面上拉了一会,跳到了详情页面,然后返回列表页面,需要保持列表页面在之前的的操作结果不变。这时候,我们第一时间想到的就是keep-alive组件,用该组件进行缓存列表页面。那么keep-alive内部是如何实现的呢?让我们一起来看下。

function pruneCache (keepAliveInstance: any, filter: Function) {
  const { cache, keys, _vnode } = keepAliveInstance
  for (const key in cache) {
    //取出缓存中的页面 
    const cachedNode: ?VNode = cache[key]
    if (cachedNode) {
      const name: ?string = getComponentName(cachedNode.componentOptions)
      //如果组件存在 且不满足缓存条件 那么清除
      if (name && !filter(name)) {
        pruneCacheEntry(cache, key, keys, _vnode)
      }
    }
  }
}
//清除缓存组件
function pruneCacheEntry (
  cache: VNodeCache,
  key: string,
  keys: Array<string>,
  current?: VNode
) {
  const cached = cache[key]
  if (cached && (!current || cached.tag !== current.tag)) {
    cached.componentInstance.$destroy()
  }
  cache[key] = null
  remove(keys, key)
}

export default {
  name: 'keep-alive',
  abstract: true,

  props: {
    include: patternTypes,
    exclude: patternTypes,
    max: [String, Number]
  },

  created () {
    this.cache = Object.create(null)
    this.keys = []
  },
  //页面销毁的时候,删除缓存组件及对应的key
  destroyed () {
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this.keys)
    }
  },

  mounted () {
    //加载的时候 从缓存中取出来即将加载的组件
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },

  render () {
    const slot = this.$slots.default
    const vnode: VNode = getFirstComponentChild(slot)
    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
    if (componentOptions) {
      // check pattern
      const name: ?string = getComponentName(componentOptions)
      const { include, exclude } = this
      if (
        // not included
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }

      const { cache, keys } = this
      const key: ?string = vnode.key == null
        // same constructor may get registered as different local components
        // so cid alone is not enough (#3269)
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key
      if (cache[key]) {
        //如果组件存在缓存中,则将其取出,并置顶
          //LRU 算法,Least Recently Used 将最近使用的移动到哈希链表末尾 如果链表超过预期长度,将最不常用的删除
        vnode.componentInstance = cache[key].componentInstance
        // make current key freshest
        remove(keys, key)
        keys.push(key)
      } else {
        //如果尚未缓存,将其缓存。如果缓存组件已超出最大值,清除队首组件
        cache[key] = vnode
        keys.push(key)
        // prune oldest entry
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
      }

      vnode.data.keepAlive = true
    }
    return vnode || (slot && slot[0])
  }
}

在上述keep-alive的代码实现中,vue使用了LRU缓存算法(LRU,Least Recently Used)将最后使用组件放在队尾位置,将不常使用的组件放在队首位置,一旦缓存即将超出最大值限制,将队首缓存的组件清除,将新的组件缓存。

Virtual-Dom

什么是Virtual-Dom

Virtual-Dom是时代化的产物。
在web早期,页面的交互效果比现在简单很多,也没有那么多的状态需要管理,所以也不需要频繁的操作Dom,使用jQuery就可以解决大部分问题。但是随着时代的发展,我们的页面功能越来越多,产品需求越来越复杂,随之而来的就是页面交互更多、状态越来越难以管理,对Dom的操作也更加频繁。如果我们仍然使用之前的方式进行开发,那么就有可能导致我们的项目逻辑越来越复杂,后期维护成本剧增,甚至出现一些难以维护的代码。

这就是命令式操作Dom的缺点。在当下业务越来越复杂的情况下,它有着难以言喻的痛点。

现在我们使用的前端三大框架:React、Vue、Angular。它们都是生命式的操作Dom,通过生命式的描述状态和Dom之间的映射关系,使用这种映射就可以将状态渲染成视图。至于关于状态到视图的转换操作,框架会在底层帮我们实现,开发者无需手动操作Dom。

DOM操作很慢是两个原因,一个是本身操作就不快,第二是我们(还有很多框架)处理dom的方式很慢,Virtual Dom解决了我们对Dom的低劣操作,它让我们不需要进行Dom操作,而是将希望展现的最终结果告诉 VueVue通过一个简化的DomVirtual dom进行 render,当你试图改变显示内容时,新生成的Virtual Dom会 与现在的Virtual dom对比,通过diff算法找到区别,这些操作 都是在快速的js中完成的,最后对实际Dom进行最小的Dom操作来完成效果,这就是Virtual Dom的概念。 rective(descriptor, this, node, host, scope, frag)
image.png
这仅仅是第一层。真正的 DOM 元素非常庞大,这是因为标准就是这么设计的。而且操作它们的时候 我们要小心翼翼,轻微的触碰可能就会导致页面重排,这可是杀死性能的罪魁祸首。
Virtual-dom是一系列的模块集合,用来提供声明式的DOM渲染。来看一个简单的 DOM 片段. 本质上就是在 JSDOM 之间做了一个缓存。可以类比 CPU 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 DOM 这么慢,我们就在它们 JSDOM 之间加个缓存。CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM)。

Vue.js中的Virtual-Dom

在Vue.js中使用模版来描述状态和Dom之间的联系。Vue.js通过编译将模版转换成渲染函数Render,然后执行渲染函数即可得到一个Virtual-Dom,通过这个Virtual-Dom即可渲染页面。
image.png
Virtual-Dom的目标就是将vnode渲染到dom上。但是如果直接进行渲染的话,会有一些不必要的麻烦。例如:

<ul>
  <li></li>
  <li></li>
  <li></li>
</ul>

如果只有一个li发生了变化,此时根本无需将整个ul替换掉,这样可以避免很大的性能浪费。所以,在vue内部实现时,在更新页面之前,会将vnode和上一次渲染的node节点(oldVNode)进行比较,找出真正需要更新的节点,从而避免操作其他dom节点。
image.png
可以看出,VDOM在vue中一共做了两件事:

  • 提供和真实Dom对应的Virtual-Dom
  • 使用vnode和oldVNpde进行比较,更新视图

其中,vnode只是一个JavaScript对象,它上面绑定了更新Dom所需要的一些数据。对两个vnode、oldVNode进行对比的核心是patch,它在内部判断是哪些节点发生了变化,从而对发生了变化的节点进行更新。

小结

Virtual-Dom是将状态映射成视图的一种解决方案。它的工作原理是使用状态生成vnode,然后使用vnode更新视图。
之所以使用状态先生成vnode,是因为如果直接使用状态改变真是Dom,会有一定程度的性能浪费。而先生成vnode,可以先将vnode缓存,在和上一次生成的渲染时缓存的vnode进行比较,找出真正需要更新的节点,然后更新视图。这样可以避免一部分不必要的Dom操作,节省一部分的性能开销。

vue通过模版来描述状态和视图之间的映射关系,所以它会首先将模版编译成渲染函数Render,执行渲染函数生成vnode,进而更新视图。

因此Virtual-Dom在vue中所做的就是提供和真实Dom对应的vnode,将vnode和oldVNode进行对比,根据对比结果进而更新视图。

VNode

什么是vnode

在Vue.js中存在一个VNode类,在类中定义了VNode的属性的方法。使用该类可以实例化出不同的vnode类型,而不同的vnode类型则表示着不同的dom节点。

export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node

  // strictly internal
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?
  asyncFactory: Function | void; // async component factory function
  asyncMeta: Object | void;
  isAsyncPlaceholder: boolean;
  ssrContext: Object | void;
  fnContext: Component | void; // real context vm for functional nodes
  fnOptions: ?ComponentOptions; // for SSR caching
  devtoolsMeta: ?Object; // used to store functional render context for devtools
  fnScopeId: ?string; // functional scope id support

  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
    this.ns = undefined
    this.context = context
    this.fnContext = undefined
    this.fnOptions = undefined
    this.fnScopeId = undefined
    this.key = data && data.key
    this.componentOptions = componentOptions
    this.componentInstance = undefined
    this.parent = undefined
    this.raw = false
    this.isStatic = false
    this.isRootInsert = true
    this.isComment = false
    this.isCloned = false
    this.isOnce = false
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
  }

  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child (): Component | void {
    return this.componentInstance
  }
}

从上面的代码中我们可以看出,vnode只是一个JavaScript对象,是从VNode实例化出来的一个实例,只是使用了JavaScript对象进行了描述,所以dom元素上所存在的所有的属性,在vnode上都可以找到。

VNode的作用

因为每次渲染视图时都需要先创建vnode,然后再用它创建dom插入视图中,所以我们可以将vnode缓存起来,这样,当每次创建vnode时,将新创建的vnode和上次渲染时创建的vnode进行对比,查找它们之间的不同之处,就可以获取具体需要更新的节点位置,基于此再去更新dom。
而且在vue2.0中采用了中等粒度的侦测策略,只侦测到组件级别,当数据改变的时候,通知组件,由组件自身使用vnode进行视图的更新。

VNode的类型

vnode有很多不同的类型,下面让我们看下vnode不同节点类型之间的区别:

  • 注释节点
  • 文本节点
  • 元素节点
  • 组件节点
  • 函数式组件
  • 克隆节点

上面我们介绍过vnode只是一个普通的JavaScript对象类型而已,而不同的节点类型只是属性不同,准确来说是有效属性不同,当实例化一个vnode时,通过参数来设置有效属性,无效的属性会被默认为undefined或null。对应无效的属性,忽略即可。

注释节点

注释节点的创建很简单,我们直接通过代码查看即可。

export const createEmptyVNode = (text: string = '') => {
  const node = new VNode()
  node.text = text
  node.isComment = true
  return node
}

我们可以看到一个注释节点只有两个有效属性:

  • text:注释文本内容
  • isComment

其他属性均为undefined或者null
例如:

<!--这是一个注释节点-->
{
    text:"这是一个注释节点",
  isComment:true
}
文本节点

文本节点的创建也很简单,直接看代码。

export function createTextVNode (val: string | number) {
  return new VNode(undefined, undefined, undefined, String(val))
}

结合上面的VNode类一起看,我们发现文本节点只有一个有效属性:text。

克隆节点

克隆节点是将现有节点进行克隆,克隆之后具备现有节点的一切属性。它的作用是优化静态节点和插槽节点(slot node)。

// optimized shallow clone
// used for static nodes and slot nodes because they may be reused across
// multiple renders, cloning them avoids errors when DOM manipulations rely
// on their elm reference.
export function cloneVNode (vnode: VNode): VNode {
  const cloned = new VNode(
    vnode.tag,
    vnode.data,
    // #7975
    // clone children array to avoid mutating original in case of cloning
    // a child.
    vnode.children && vnode.children.slice(),
    vnode.text,
    vnode.elm,
    vnode.context,
    vnode.componentOptions,
    vnode.asyncFactory
  )
  cloned.ns = vnode.ns
  cloned.isStatic = vnode.isStatic
  cloned.key = vnode.key
  cloned.isComment = vnode.isComment
  cloned.fnContext = vnode.fnContext
  cloned.fnOptions = vnode.fnOptions
  cloned.fnScopeId = vnode.fnScopeId
  cloned.asyncMeta = vnode.asyncMeta
  cloned.isCloned = true
  return cloned
}

可以看出,在克隆时,只需要将被克隆的节点的所有属性赋值到新节点即可。克隆节点和原节点唯一的不同是克隆节点的isClone为true,而被克隆节点的isClone为false。

元素节点

元素节点通常存在以下四个属性:

  • tag:表示当前节点的名称,如p、div等
  • data:表示当前节点上的数据,如style、attrs等
  • children:表示当前节点的子节点列表
  • context:表示当前节点的vue实例

例如:

<div>
  <p>第一个子节点</p>
  <span>第二个自节点</span>
</div>

其对应的vnode:

{
        tag:'div',
    data:{...},
    children:[...],
    context:{...}
}
组件节点

组件节点有以下两个属性:

  • componentOptions:指的是当前组件节点的选项参数,如propData、tag、children等
  • componentInstance:组件的实例,即vue的实例。其实每一个组件都是一个vue实例

例如:

<child></child>

对应的组件节点:

{
  componentOptions:{...},
  componentInstance:{...},
    context:{...},
  data:{...},
  tag:'vue-component-1-child'
}
函数式组件

函数式组件和组件节点类似,但是它有两个独有的属性:functionialContext 和 functionialOptions。

小结

VNode是一个类,可以用于生成不同类型的vnode。而不同类型的vnode代表着不同的真是的dom类型。
Vue.js中并不是直接更新dom节点,而是先生成vnode,将新创建的vnode和上次渲染生成的vnode进行比较,找出真实需要更新的节点,节省了性能的开销。
vnode有很多种类型,它们本质上都是从VNode中实例化出来的对象,只是有效属性不同而已。

Patch


















webpack性能优化

基于webpack的性能优化

基本优化方案

  1. 使用高版本的webpack和node
  2. 静态资源优化
    1. JavaScript文件压缩、合并、TreeShaking
    2. CSS文件压缩、优化、TreeShaking
    3. Img压缩、合并雪碧图:
    4. HTML文件压缩
    5. 上传CDN
    6. 使用prefetch、preload预加载资源文件
      1. preload:本次加载需要的资源,告诉浏览器,提高下载速度权重
      2. prefetch:未来可能使用到的资源文件,在浏览器空闲的时候下载
  3. 代码分割Code Spliting
    1. entry:入口文件手动分割
    2. splitChunks:optimization.splitChunks,将相同的代码抽离到一个单独的文件;也可以将第三方库文件抽离
    3. import():动态导入。
  4. 利用缓存
    1. 使用cache-loader,缓存每次打包之后的文件。
  5. 多核
    1. 使用happypack(已不维护)、thread-laoderwebpack-parallel-uglify-plugin等,开启多核执行。
  6. 分包加载:使用DllPlugin 和 DLLReferencePlugin
  7. 缩小搜索范围:使用exclude、include尽量缩小webapck的搜索范围。
  8. 配置resolve .extensions 尽量将优先级高的放在前面,缩短命中时间。
  9. 预渲染:prenderer-spa-plugin 将重要页面采用预渲染功能,有利于SEO。
  10. 服务端渲染:vue-server-render 在服务端将页面渲染完成,直接返回html页面给客户端。
  11. 同构:SSR + CSR,刷新SSR ,路由切换CSR。

静态资源优化

JavaScript

使用DllPluginDLLReferencePlugin,拆分bundles。具体使用请看这一篇文章。也可以使用hard-source-webpack-plugin替换DllPluginDLLReferencePlugin
生产环境webpack默认开启uglify-js

压缩js
optimization: {
    minimizer: [
      new UglifyJsPlugin({
        uglifyOptions: {
          mangle: {
            safari10: true
          },
          // 清除生产环境的控制台输出
          compress: {
            warnings: false,
            drop_debugger: true,
            drop_console: true
          }  
        },
        sourceMap: config.build.productionSourceMap,
        cache: true,
        parallel: true
      })
    ]
  }
抽离公共js文件
splitChunks: {
    chunks: "all",//进行代码分割的时候,all:针对所有的导入  async:只针对异步导入 initial:针对同步代码导入。
    minSize: 30000,//设置最小阀值,只有大于该阀值,才会进行代码分割。
    minChunks: 1,//在分割模块之前共享一个模块的最小块数(设置代码最少被引用次数)
    maxAsyncRequests: 5,//按需加载时的最大并行请求数 超过就不会在做代码分割打包
    maxInitialRequests: 3,//一个入口点的最大并行请求数  超过就不会做代码分割
    automaticNameDelimiter: '~',//打包生成之后,默认情况下,webpack将使用块的来源和名称来生成名称,比如vendor~main.js。
    name: true,//使得cacheGroups中打包生成的文件名称
    cacheGroups: {//缓存组 打包分组
      vendors: {//配置同步导入 
        test: /[\\/]node_modules[\\/]/,//只有node_module中的才会进入
        priority: -10,//值越大 优先级越高
        filename: 'vendors.js'
      },
      default: {
        // minChunks: 2,
        priority: 0,
        reuseExistingChunk: true,// 如果模块已经被打包了,在此遇到的时候 直接忽略,直接使用以打包好的模块。
        filename:'default.js'
      }
    }
  }
关于代码分离

请移步https://www.yuque.com/walter-glskq/zf7grn/zcamxf

CSS

extract-text-webpack-pluginmini-css-extract-plugin使用该插件将css文件从js中剥离出来,进行优化压缩。配合postcss-loader进行css预处理和后处理。

//压缩css 并且将css从js抽离出来
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
//优化css
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');

module.exports = {
    module: {
   rules: [{
             test: /\.css$/,
       use: [{
         loader: MiniCssExtractPlugin.loader,
         options: {
           esModule: true,
           filename: '/css/[name].[hash:7].css'
         },
       }, 'css-loader', 'postcss-loader','less-loader']
   }]
 },
  plugins:[
      new MiniCssExtractPlugin({
      filename: 'css/[name].[hash:5].css',
      chunkFilename: 'css/[id].[hash:5].css',
      ignoreOrder: false
    }),
    new OptimizeCssAssetsPlugin({
      assetNameRegExp: /\.css$/g,
      cssProcessor: require('cssnano'),
      cssProcessorPluginOptions: {
        preset: ['default', { discardComments: { removeAll: true } }],
      },
      canPrint: true
    })
  ]
}

Img

使用file-loader和image-webpack-loader处理图片文件,进行压缩。

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /.*\.(gif|png|jpe?g|svg|webp)$/i,
        use: [
          {
            loader: 'file-loader',
            options: {}
          },
          {
            loader: 'image-webpack-loader',
            options: {
              mozjpeg: { // 压缩 jpeg 的配置
                progressive: true,
                quality: 65
              },
              optipng: { // 使用 imagemin-optipng 压缩 png,enable: false 为关闭
                enabled: false,
              },
              pngquant: { // 使用 imagemin-pngquant 压缩 png
                quality: '65-90',
                speed: 4
              },
              gifsicle: { // 压缩 gif 的配置
                interlaced: false,
              },
              webp: { // 开启 webp,会把 jpg 和 png 图片压缩为 webp 格式
                quality: 75
              },
          },
        ],
      },
    ],
  },
}

使用url-loader将一些小图片转为base64格式,减少http请求:

{
  test: /\.(png|jpg|jpeg|gif)(\?.+)?$/,
        exclude: /favicon\.png$/,
      use: [{
        loader: 'url-loader',
        options: { //动态抽离img 给一个阀值 小于该阀值的 以base64的形式存在与js中
          esModule: false,
          limit: 10000,
          name: 'image/[name].[hash:7].[ext]'
        }
      }]
}

压缩HTML

配置minify,优化html文件。生产环境生效。

new HtmlWebpackPlugin({
  filename: 'index.html',
  template: 'index.html',
  inject: true,
  title: 'admin',
  minify: {
    // 合并空格
    collapseWhitespace: true,
    // 移除注解
    removeComments: true,
    // 移除多余的属性
    removeRedundantAttributes: true,
    // 移除脚本类型属性
    removeScriptTypeAttributes: true,
    // 移除样式类型属性
    removeStyleLinkTypeAttributes: true,
    // 使用简短的文档类型
    useShortDoctype: true
    // more options:
    // https://github.com/kangax/html-minifier#options-quick-reference
  }
}),

使用DllPlugin和DLLReferencePlugin

  1. 使用DllPluginDLLReferencePlugin
    1. DLLPluginDLLReferencePlugin 用某种方法实现了拆分 bundles,同时还大大提升了构建的速度。

包含大量复用模块的动态链接库只需被编译一次,在之后的构建过程中被动态链接库包含的模块将不会重新编译,而是直接使用动态链接库中 的代码 由于动态链接库中大多数包含的是常用的第三方模块。例如 vue、vue-router ,所以只要不升级这些模块的版本,动态链接库就不用重新编译。

优化Loader配置

由于 Loader 对文件的转换操作很耗时,所以需要让尽可能少的文件被 Loader 处理。可以通过 test include exclude 三个配置项来命中 Loader 要应用规则的文件。

module.exports = { 
  module : { 
    rules : [{
      //如果项目源码中只有js文件,就不要写成/\jsx?$/,以提升正则表达式的性能
      test: /\.js$/, 
      //babel -loader 支持缓存转换出的结果,通过 cacheDirectory 选项开启
      use: ['babel-loader?cacheDirectory'] , 
      //只对项目根目录下 src 目录中的文件采用 babel-loader
      include: path.resolve(__dirname,'src'),
    }],
  }
}

优化 resolve.extensions 配置

在导入语句没带文件后缀时,Webpack 会自动带上后缀去尝试询问文件是否存在。如果这个列表越长,或者正确的后缀越往后,就会造成尝试的次数越多,所以resolve .extensions 的配置也会影响到构建的性能。
建议:

  • 后缀尝试列表要尽可能小,不要将项目中不可能存在的情况写到后缀尝试列表中。
  • 频率出现最高的文件后缀要优先放在最前面,以做到尽快退出寻找过程。
  • 在源码中写导入语句时,要尽可能带上后缀 从而可以避免寻找过程
module.exports = { 
  resolve : { 
    //尽可能减少后缀尝试的可能性
    extensions : ['js'],
  }
}

优化打包加速

使用webpack-parallel-uglify-plugin。webpack默认提供了UglifyJS插件来压缩JS代码,但是它使用的是单线程压缩代码,也就是说多个js文件需要被压缩,它需要一个个文件进行压缩。所以说在正式环境打包压缩代码速度非常慢(因为压缩JS代码需要先把代码解析成用Object抽象表示的AST语法树,再去应用各种规则分析和处理AST,导致这个过程耗时非常大)。当webpack有多个JS文件需要输出和压缩时候,原来会使用UglifyJS去一个个压缩并且输出,但是ParallelUglifyPlugin插件则会开启多个子进程,把对多个文件压缩的工作分别给多个子进程去完成,但是每个子进程还是通过UglifyJS去压缩代码。无非就是变成了并行处理该压缩了,并行处理多个子任务,效率会更加的提高。

// 引入 ParallelUglifyPlugin 插件
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');

module.exports = {
  plugins: [
    // 使用 ParallelUglifyPlugin 并行压缩输出JS代码
    new ParallelUglifyPlugin({
      // 传递给 UglifyJS的参数如下:
      uglifyJS: {
        output: {
          /*
           是否输出可读性较强的代码,即会保留空格和制表符,默认为输出,为了达到更好的压缩效果,
           可以设置为false
          */
          beautify: false,
          /*
           是否保留代码中的注释,默认为保留,为了达到更好的压缩效果,可以设置为false
          */
          comments: false
        },
        compress: {
          /*
           是否在UglifyJS删除没有用到的代码时输出警告信息,默认为输出,可以设置为false关闭这些作用
           不大的警告
          */
          warnings: false,

          /*
           是否删除代码中所有的console语句,默认为不删除,开启后,会删除所有的console语句
          */
          drop_console: true,

          /*
           是否内嵌虽然已经定义了,但是只用到一次的变量,比如将 var x = 1; y = x, 转换成 y = 1, 默认为不
           转换,为了达到更好的压缩效果,可以设置为false
          */
          collapse_vars: true,

          /*
           是否提取出现了多次但是没有定义成变量去引用的静态值,比如将 x = 'xxx'; y = 'xxx'  转换成
           var a = 'xxxx'; x = a; y = a; 默认为不转换,为了达到更好的压缩效果,可以设置为false
          */
          reduce_vars: true
        }
      }
    }),
  ]
}
在通过 new ParallelUglifyPlugin() 实列化时,支持以下参数配置如下:

test: 使用正则去匹配哪些文件需要被 ParallelUglifyPlugin 压缩,默认是 /.js$/.
include: 使用正则去包含被 ParallelUglifyPlugin 压缩的文件,默认为 [].
exclude: 使用正则去不包含被 ParallelUglifyPlugin 压缩的文件,默认为 [].
cacheDir: 缓存压缩后的结果,下次遇到一样的输入时直接从缓存中获取压缩后的结果并返回,cacheDir 用于配置缓存存放的目录路径。默认不会缓存,想开启缓存请设置一个目录路径。

workerCount:开启几个子进程去并发的执行压缩。默认是当前运行电脑的 CPU 核数减去1。
sourceMap:是否为压缩后的代码生成对应的Source Map, 默认不生成,开启后耗时会大大增加,一般不会将压缩后的代码的
sourceMap发送给网站用户的浏览器。
uglifyJS:用于压缩 ES5 代码时的配置,Object 类型,直接透传给 UglifyJS 的参数。

优化小插件

speed-measure-webpack-plugin

//添加打包时各种资源构建小时钟提示 有助于分析
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
module.exports = smp.wrap({})

webpack-bundle-analyzer

打包分析工具

npm install --save-dev webpack-bundle-analyzer

在webpack的plugins中配置: new BundleAnalyzerPlugin()package.json 中增加: "analyz": "NODE_ENV=production npm_config_report=true npm run build"

webpack-dashboard

更加直观的显示webpack的构建。

npm install webpack-dashboard --save-dev

const Dashboard = require('webpack-dashboard');
const DashboardPlugin = require('webpack-dashboard/plugin');
const dashboard = new Dashboard();

plugins: [
  new DashboardPlugin(dashboard.setData)
]

image.png

Cookie / Session安全机制

原文:https://harttle.land/2015/08/10/cookie-session.html

Cookie 和 Session是为了在无状态的HTTP协议上维护会话状态,使得服务器知道当前是和哪个用户打交道。

Cookie和Session

因为HTTP协议是无状态的,即每次请求到达服务器端的时候,服务端是无法知道当前用户的身份、是否登录等。现在之所以服务器知道用户的身份是因为服务器在用户第一次请求的时候设置了Cookie。而Session是借助Cookie而实现的在服务器和浏览器直接的会话。

image.png

Cookie的实现机制

Cookie是由客户端保持的一个文本文件,其内容是一系列的键值对。Cookie是由服务器端设置,保存在浏览器端的。在用户访问服务器的时候,会在HTTP请求头中携带当前的Cookie。下面我们看一下Cookie的工作流程:

  • 用户访问某个页面,浏览器向服务器发起请求
  • 服务器端获取浏览器的请求,并基于浏览器响应
  • 服务器端在响应头中添加Cookie:set-cookie:,其值为要设置的cookie的键值对,如:path、expires、domain等。

RFC2109 6.3 Implementation Limits 中提到: UserAgent(浏览器就是一种用户代理)至少应支持300项Cookie, 每项至少应支持到4096字节,每个域名至少支持20项Cookie。

  • 浏览器接收到服务器的响应,并解析。
  • 当遇到set-cookie时,就会将其值保存在内存或者磁盘中。
  • 浏览器下次给该服务器发送HTTP请求时, 会将服务器设置的Cookie附加在HTTP请求的头字段Cookie中。

浏览器可以存储多个域名下的Cookie,但只发送当前请求的域名曾经指定的Cookie, 这个域名也可以在Set-Cookie字段中指定)。

  • 服务器接收到浏览器的请求,发现请求头中存在cookie字段的时候,就会确定和该用户已有过交互。
  • 浏览器会自动清除过期的cookie。

总之,服务器通过Set-Cookie响应头字段来指示浏览器保存Cookie, 浏览器通过Cookie请求头字段来告诉服务器之前的状态。 Cookie中包含若干个键值对,每个键值对可以设置过期时间。

Cookie的安全隐患

我们知道可以发送HTTP请求的不只是浏览器,很多HTTP客户端软件(包括curl、Node.js)都可以发送任意的HTTP请求,可以设置任何头字段。 假如我们直接设置Cookie字段并发送该HTTP请求, 服务器岂不是被欺骗了?这种攻击非常容易,Cookie是可以被篡改的!
**

Cookie的防篡改机制

服务器可以为每个Cookie项生成签名,由于用户篡改Cookie后无法生成对应的签名, 服务器便可以得知用户对Cookie进行了篡改。一个简单的校验过程可能是这样的:

  1. 在服务器中配置一个不为人知的字符串(我们叫它Secret),比如:x$sfz32
  2. 当服务器需要设置Cookie时(比如authed=false),不仅设置authed的值为false, 在值的后面进一步设置一个签名,最终设置的Cookie是authed=false|6hTiBl7lVpd1P
  3. 签名6hTiBl7lVpd1P是这样生成的:Hash('x$sfz32'+'false')。 要设置的值与Secret相加再取哈希。
  4. 用户收到HTTP响应并发现头字段Set-Cookie: authed=false|6hTiBl7lVpd1P
  5. 用户在发送HTTP请求时,篡改了authed值,设置头字段Cookie: authed=true|???。 因为用户不知道Secret,无法生成签名,只能随便填一个。
  6. 服务器收到HTTP请求,发现Cookie: authed=true|???。服务器开始进行校验: Hash('true'+'x$sfz32'),便会发现用户提供的签名不正确。

通过给Cookie添加签名,使得服务器得以知道Cookie被篡改。然而故事并未结束。
因为Cookie是明文传输的, 只要服务器设置过一次authed=true|xxxx我不就知道true的签名是xxxx了么, 以后就可以用这个签名来欺骗服务器了。因此Cookie中最好不要放敏感数据。 一般来讲Cookie中只会放一个Session Id,而Session存储在服务器端。

Session实现机制

Session 是存储在服务器端的,避免了在客户端Cookie中存储敏感数据。 Session 可以存储在HTTP服务器的内存中,也可以存在内存数据库(如redis)中, 对于重量级的应用甚至可以存储在数据库中。
我们以存储在redis中的Session为例,还是考察如何验证用户登录状态的问题。

  1. 用户提交包含用户名和密码的表单,发送HTTP请求。

  2. 服务器验证用户发来的用户名密码。

  3. 如果正确则把当前用户名(通常是用户对象)存储到redis中,并生成它在redis中的ID。
    这个ID称为Session ID,通过Session ID可以从Redis中取出对应的用户对象, 敏感数据(比如authed=true)都存储在这个用户对象中。

  4. 设置Cookie为sessionId=xxxxxx|checksum并发送HTTP响应, 仍然为每一项Cookie都设置签名。

  5. 用户收到HTTP响应后,便看不到任何敏感数据了。在此后的请求中发送该Cookie给服务器。

  6. 服务器收到此后的HTTP请求后,发现Cookie中有SessionID,进行放篡改验证。

  7. 如果通过了验证,根据该ID从Redis中取出对应的用户对象, 查看该对象的状态并继续执行业务逻辑。

Web应用框架都会实现上述过程,在Web应用中可以直接获得当前用户。 相当于在HTTP协议之上,通过Cookie实现了持久的会话。这个会话便称为Session。

Flex布局

Flex布局

flex布局

flex是flexible box的缩写,意为“弹性盒子”,用来给盒模型提供最大的灵活性。
任何一个容器都可以指定为felx布局。

.box{
    display:flex;
  /*webkit内核*/
  display:-webkit-flex;
}
/*行内元素*/
.inline{
    display:inline-flex;
}

注意,设为 Flex 布局以后,子元素的floatclearvertical-align属性将失效

基本概念

采用flex布局的元素,称为flex容器,容器内的每一个元素自动成容器成员,称为flex 项目,简称“项目”。
容器默认存在两根轴:水平的主轴(main axis)和 垂直的交叉轴(cross axis)。主轴的开始位置(与边框的交叉点)叫做main start,结束位置叫做main end;交叉轴的开始位置叫做cross start,结束位置叫做cross end。

项目默认沿主轴排列。单个项目占据的主轴空间叫做main size,占据的交叉轴空间叫做cross size。

容器属性

以下属性设置在容器上。

flex-direction
flex-wrap
flex-flow
justify-content
align-items
align-content

flex-direction

该属性决定主轴的方向,即项目的排列方向。

.box {
  flex-direction: row | row-reverse | column | column-reverse;
}

属性值有四个:

row(默认值):主轴为水平方向,起点在左端。
row-reverse:主轴为水平方向,起点在右端。
column:主轴为垂直方向,起点在上沿。
column-reverse:主轴为垂直方向,起点在下沿。

flex-wrap

默认情况下,项目都排在一条线(又称”轴线”)上。flex-wrap属性定义,如果一条轴线排不下,如何换行。

.box{
  flex-wrap: nowrap | wrap | wrap-reverse;
}

有三个取值:

nowrap: 默认。不换行。
wrap:换行,第一行在上方。
wrap-reverse:换行,第一行在下方。

flex-flow

flex-flow属性是flex-direction属性和flex-wrap属性的简写形式,默认值为row nowrap。

.box {
  flex-flow: <flex-direction> || <flex-wrap>;
}

justify-content

该属性定义了项目在主轴上的对其方式。

.box {
  justify-content: flex-start | flex-end | center | space-between | space-around;
}

该属性有五个取值:

flex-start(默认值):左对齐
flex-end:右对齐
center: 居中
space-between:两端对齐,项目之间的间隔都相等。
space-around:每个项目两侧的间隔相等。所以,项目之间的间隔比项目与边框的间隔大一倍。

align-items

该属性定义了项目在交叉轴如何对齐。

.box{
    align-items:flex-start | flex-end | center | baseline | stretch
}

该属性有五个取值:

flex-start: 交叉轴起点对齐。
flesx-end:交叉轴终点对齐。
center:交叉轴中点对齐。
baseline:项目的第一行文字的基线对齐。
stretch:如果项目未设置高度或者设置为auto,将占满整个容器的高度。

align-content

该属性定义了多根轴线的对齐方式。如果该项目只有一根轴线,则不起作用。

.box{
    align-content: felx-start | flex-end | center | space-between | space-around | stretch
}
flex-start:与交叉轴的起点对齐。
flex-end:与交叉轴的终点对齐。
center:与交叉轴的中点对齐。
space-between:与交叉轴两端对齐,轴线之间的间隔平均分布。
space-around:每根轴线两侧的间隔都相等。所以,轴线之间的间隔比轴线与边框的间隔大一倍。
stretch(默认值):轴线占满整个交叉轴。

项目属性

以下属性定义在项目上:

order
flex-grow
flex-shrink
flex-basis
flex
align-self

order

该属性定义项目的排列顺序。数值越小,排列越靠前,默认为0.

.item{
    order:<integer>
}

flex-grow

该属性定义了项目的放大比例,默认为0,即如果存在剩余空间,也不放大。

.item{
    flex-grow:<number>
}

flex-shrink

该属性定义了项目的缩小比例,默认为1,即如果空间不足,则缩小。

.item{
    flex-shrink:<number>
}

当所有的项目的flex-shrink的值都为1,则当项空间不足时,等比例缩小。如果一个项目的flex-shrink值为0,其他项目为1,则空间不足时,前者不缩小。

flex-basis

该属性定义了在分配剩余空间之前,项目占据的主轴空间,浏览器将根据该属性计算主轴是否有多余空间。默认为auto,即项目本来大小。

.item{
    flex-basis:<length> | auto
}

flex

该属性是flex-grow、flex-shrink和flex-basis的简写。默认值为 0 1 auto,后两个属性可选

.item{
    flex: none | <flex-grow> <flex-shrink> <flex-basis> 
}

该属性有两个快捷键:auto(1 1 auto) 和 none (0 0 auto)

align-self

align-self属性允许单个项目有与其他项目不一样的对齐方式,可覆盖align-items属性。默认值为auto,表示继承父元素的align-items属性,如果没有父元素,则等同于stretch

.item {
  align-self: auto | flex-start | flex-end | center | baseline | stretch;
}

该属性可能取6个值,除了auto,其他都与align-items属性完全一致。

vue源码解读(一):Observe

vue源码解读(一):Observe

Object

变化侦测

Vue.js会根据数据状态自动生成Dom,并将其渲染到页面之上。Vue.js的渲染是生命式的,我们通过模版来描述状态和Dom之间的联系。
通常,在运行时应用内部的状态是不断变化的,那么就需要不停的去渲染页面,但是这个过程中,状态发生了怎样的变化呢?
变化侦测就是用来解决这样的问题。它分为两种类型:”推” 和” 拉”。
其中AngularReact的变化侦测属于”拉”的类型。当数据发生改变的时候,会发送信号给框架,然后框架内部会进行一个暴力的对比,来找出哪些节点需要重新渲染,这就是Angular的脏检测。而React使用的则是虚拟Dom。
Vue.js使用的则是”推”类型。当一个状态发生改变之后,它会自动的发送通知给每一个依赖,让它们进行Dom更新。但是这也是有代价的,当一个状态的依赖越来越多的时候,这种推的过程的内存开销就会随之增大,所以在Vue2.0开始,引入来虚拟Dom,将粒度调整为中等粒度,每一个状态所绑定的依赖不再是具体的节点,而是组件。这样当状态发生变化之后,只需要通知对应的组件,由组件内部使用虚拟Dom进行比较。这样大大的降低来依赖数量,从而降低来依赖跟踪的内存消耗。

如何追踪变化

追踪一个对象的变化,有两个方法:Object.definePrototype()ES6的proxy。而由于ES6在各浏览器的支持度并不理想,所以在Vue2.0中,依旧使用的是Object.definePrototype()。但是在Vue3.0中,使用的就是proxy了。
根据Object.definePrototype()我们可以封装一个函数,来定义一个响应式数据对象。

function defineReactive(data,key,val){
    Object.defineProperty(data,key,{
        configurable:true,
        enumerable:true,
        get:function(){
            return val
        },
        set:function(newVal){
            if(val == newVal){
                return
            }
            val = newVal;
        }
    })
}
let obj = {
    a:10,
    b:20
}
defineReactive(obj,'a');
obj.a = 200;
console.log(obj.a);

只要调用函数defineReactive,传入参数data,key和val,即可对data进行监听。当获取data的key属性时,就会触发get;设置key属性时就会触发set。

收集依赖

所谓依赖,就是和目标对象的属性有一定的联系。而上面我们也通过一个栗子看到来,当我们获取一个对象的属性的时候,对象的访问器属性get就会被触发,那么我们可以不可以在这里进行依赖搜集?当get被触发的时候,将用到该属性的地方搜集起来,等到设置(修改属性值)的时候,即set被触发的时候,通知每一个依赖。
所以,最后我们得出结论:对象的监测,是在getter中收集依赖,在setter中触发依赖。
**

依赖保存在哪里

上一步我们已经知道如何搜集依赖,那么依赖搜集之后,我们将依赖放在哪里呢?
思考一下,如果以每一个key都对应一个数组,当key对应的值发生改变之后,遍历数组,触发依赖,那么是不是就可以来?
现在我们假设依赖是一个函数,保存在window.target上,那么:
修改上面的函数:

function defineReactive(data,key,val){
      let dep = [];//依赖
    Object.defineProperty(data,key,{
        configurable:true,
        enumerable:true,
        get:function(){
              dep.push(window.target);
            return val
        },
        set:function(newVal){
            if(val == newVal){
                return
            }
              for(let i = 0,len = dep.length; i < len; i++){
                    dep[i](newVal,val);
            }
            val = newVal;
        }
    })
}

这里我们新增数组dep作为存储搜集的依赖。然后当set被触发的时候,遍历dep,触发依赖。
然后我们观测以上代码,我们在变化侦测的方法中,进行依赖搜集的触发,这样写的化,代码的耦合度显得有点高,所以我们需要解耦,将dep抽离出来,单独封装成一个类:

export default class Dep {
    constructor(){
        this.subs = [];
    }
    addSub(sub){
        this.subs.push(sub)
    }
    removeSub(sub){
        let index = this.subs.indexOf(sub);
        if(index > 0){
            this.subs.splice(index,1);
        }
    }
    depend(){
        if(window.target){
            this.addSub(window.target)
        }
    }
    notify(){
        for (let index = 0; index < this.subs.length; index++) {
            const element = this.subs[index];
            element.update()
        }
    }
}

Dep.target = null
const targetStack = []

export function pushTarget (target) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

然后改造defineReactive:

import Dep from './dep'
function defineReactive(data,key,val){
      let dep = new Dep();//依赖
    Object.defineProperty(data,key,{
        configurable:true,
        enumerable:true,
        get:function(){
              dep.depend();
            return val
        },
        set:function(newVal){
            if(val == newVal){
                return
            }
              dep.notify();
            val = newVal;
        }
    })
}

依赖是谁

上面我们假设依赖是一个函数,window.target。那么它到底是什么?
我们知道,收集谁,就是当属性变化之后通知谁。
在Vue.js中,我们需要通知的地方,可能是模版、也可能是个watch。那么我们能不能将其抽离出来,封装成类,当属性收集的时候,只将类的实例收集起来,通知也只通知它一个,然后由它去通知其他地方。所以我们将这个类取个名字:Watcher。

什么是Watcher

在我的理解中,watcher就是一个中介,当属性发生变化的时候通知它,然后它再通知其他地方。

import { parsePath } from '../lib/parsePath';
import Dep, { pushTarget, popTarget } from './dep'

export default class Watcher {
    constructor(vm,expOrFn,cb){
        this.vm = vm;
        this.getter = parsrPath(expOrFn);
        this.cb = cb;
        this.value = this.get()
    }
    get(){
        pushTarget(this);
        // window.target = this;
        let value = this.getter.call(this.vm,this.vm);
        // window.target = undefined;
        popTarget();
        return value;
    }
    update(){
        const oldValue = this.value;
        this.value = this.get();
        this.cb.call(this.vm,this.value,oldValue)
    }
}

const bailRE = /^\w.s]/;
function parsePath(path){
    if(bailRE.test(path)){
        return
    }
    //使用.将path分割成数组,然后一层层的遍历,即可获取想读的数据
    const segments = path.split('.');
    return function(obj){
        for (let index = 0; index < segments.length; index++) {
            if(!obj) return;
            obj = obj[segments[index]];
        }
        return obj;
    }
}

在这段代码中,通过在get函数中,将this(即watcher实例)添加到Dep中,然后在读取属性值,就会触发getter,只要触发来getter就会触发依赖搜集逻辑。
将依赖注入到Dep中之后,只要属性值发生变化,就会让依赖列表中所有的依赖都执行update方法,也就是Watcher的update方法,而update方法会执行参数中的回调,将新老值传入回调中。

递归侦测所有的key

现在已经可以侦测到对象的属性值的变化,那么下一步就是将对象所有的属性都可以侦测到。那么我们就需要将其封装成一个Observer类。

class Observe {
    constructor(value) {
        this.value = value
        if (!Array.isArray(value)) {
            this.walk(value)
        }
    }
    /**
     * walk会将每一个对象的属性都转换成getter和setter的形式进行监听
     */
    walk(obj) {
        const keys = Object.keys(value)
        for (let i = 0; i < keys.length; i++) {
            defineReactive(obj, keys[i],obj[keys[i]])
        }
  }
}



function defineReactive(data, key, val) {
    if (typeof val === 'object') new Observe(val);//递归子属性
    let dep = new Dep() //依赖
    let childpOb = observe(val) //返回一个Observe实例
    Object.defineProperty(data, key, {
        configurable: true,
        enumerable: true,
        get: function() {
            //依赖搜集
            dep.depend()
            return val
        },
        set: function(newVal) {
            if (val == newVal) {
                return
            }
            val = newVal
            dep.notify()
        }
    })
}

我们定义类一个Observe类,它将一个正常的object转换为类一个被侦测的object。然后通过判断数据类型,只有Object类型的数据才会调用walk将每一个属性转换为getter/setter形式。

Object的问题

上面我们接受类Object的数据侦测的方式,是有getter/setter的方式实现的,那么正是由于这种追踪方式,使得一些属性变化不会被侦测到,如新增属性、删除属性。Vue.js的这种实现方式只能侦测一个对象的值的变化,而无法追踪新增属性和删除属性。所有才会导致上述问题。
但是在ES6之后,JavaScript提供类元编程的能力,我们可以通过proxy对Object进行元编程。具体请移步proxy

Array

数组的变化侦测

数组的变化侦测和Object的变化侦测有一点点的不同,下面我们举个🌰看看:

this.list.push(1)

在上面的代码中,我们向list数组中push了一个元素,但是这并没有触发getter/setter,所以用于Object的侦测方法已经不适合数组了。
在上面的代码中,我们还可以知道,我们通过push完成了数组的改变,那么如果我们在push的时候,获取通知,那么是不是就可以完成侦测?
所以,我们可以使用自定义的数组方法,通过覆盖数组原型的方法,来实现数组的变化侦测。如图:
image.png
我们在数组的原型前加了一层拦截器,拦截数组的操作方法,就可以实现对数组的变化侦测。

拦截器

那么拦截器是如何实现的呢?
首先,我们需要知道哪些方法可以改变数组,我们只需要在拦截器中重写这些方法覆盖数组原型对应的方法即可,没有必要对数组原型进行全方位的覆盖。
经过整理,我们知道可以改变数组自身内容的方法有:push、pop、shift、unshift、sort、reverse、slice七种方法。
所以,我们可以写出如下代码,对数组原型上的方法进行覆盖:

const originMethods = Array.prototype;
const arrayProto = Object.create(originMethods);
const methods = ['push','pop','shift','unshift','sort','reverse','slice'];

methods.forEach(method => {
  //缓存原始方法
    const originMethod = arrayProto[method];
  Object.definePrototype(arrayProto,method,{
      value:(...params) => {
      //todo
        return originMethod.apply(this,params);
    },
    enumerable:true,
    writeable:true,
    configerable:true
  })
})

在上述代码中,我们创造了arrayProto,它继承自Array.prototype,具备其一切的方法;接下来,我们通过遍历methods对其中每一个方法都进行重新封装,让其指向原型上的方法,这样我们使用拦截器拦截数组方法的操作时,使用的仍然是数组原型对象上的方法,而我们只需要在value的箭头函数中,做一些该做的事即可。

使用拦截器覆盖数组方法

有了拦截器之后,我们要做的就是使用拦截器覆盖Array.prototype,但是我们不能够进行全局覆盖,这样会污染全局的Array,而我们只希望拦截被侦测的数据,即只覆盖那些响应式的数组原型。
而将一个数据转换成响应式的,就需要使用Observe,所以我们在Observe中拦截即可。

具体请查阅源码:/src/core/observer#Observer

//判断是否支持__proto__
const hasProto = '_proto_' in {}
//如果支持_proto_属性,则使用protoAugment
function protoAugment(target, src, keys) {
    target._proto_ = src
}
//否则使用copyAugment
function copyAugment(target, src, keys) {
    // console.log(target,src,keys)
    for (let i = 0, len = keys.length; i < len; i++) {
        const key = keys[i]
        def(target, key, src[key])
    }
}

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor(value: any) {
    this.value = value;
    this.dep = new Dep();
    this.vmCount = 0;
    def(value, "__ob__", this);
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods);
      } else {
        copyAugment(value, arrayMethods, arrayKeys);
      }
      this.observeArray(value);
    } else {
      this.walk(value);
    }
  }

  /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk(obj: Object) {
    const keys = Object.keys(obj);
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i]);
    }
  }

  /**
   * Observe a list of Array items.
   */
  observeArray(items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i]);
    }
  }
}

上面我们通过上述代码,我们新增方法hasProto来判断浏览器是否支持proto属性,如果支持,则直接使用该属性设置对象的原型,否则使用递归复制到对象原型上。将拦截器设置在value.proto上之后,当用户使用这些方法之后,访问的不再是数组原型的方法,而是我们自定义的方法。
image.png

收集依赖

大家还记得我们前面提到的🌰吗?

this.list.push(1);

在这里,我们向数组中添加来一个元素。请注意,我们是在this对象上的list属性添加一个元素。所以,当我们访问list数组的时候,需要使用this.list。所以,我们就可以使用Object.definePrototype()来监听访问,当用户访问list时,一定会触发list的getter。
所以,数组的依赖收集也是在getter中进行。在拦截器中触发依赖。

依赖收集到哪里

在vue.js中依赖是被收集到Observe中的。

具体请查阅源码:/src/core/observer#defineReactive

function defineReactive(data, key, val) {
    if (typeof val === 'object') new Observe(val)
    let dep = new Dep() //依赖
    let childpOb = observe(val) //返回一个Observe实例
    Object.defineProperty(data, key, {
        configurable: true,
        enumerable: true,
        get: function() {
            console.log(val, 'get')
            //依赖搜集
            dep.depend()

            if (childpOb) {
                childpOb.dep.depend()
            }
            return val
        },
        set: function(newVal) {
            if (val == newVal) {
                return
            }
            console.log(newVal, 'set')
            // for(let i = 0; i < dep.length; i++){
            //     //遍历 依次触发依赖
            //     dep[i](newVal,val);
            // }
            val = newVal
            dep.notify()
        }
    })
}

/**
 * 尝试新建一个observe
 * 如果创建成功 返回新的observe实例
 * 否则返回已存在的observe实例
 */
function observe(value, asRootData) {
    if (!isObject(value)) return
    let ob
    //如果value已经是响应式的数据 则直接返回
    if (hasOwn(value, '_ob_') && value._ob_ instanceof Observe) {
        ob = value._ob_
    } else {
        ob = new Observe(value)
    }
    return ob
}

在上面我们创建来一个函数observe,用于新建Observe实例,如果value已经是响应式数据,即直接返回,否则创建一个新的Observe实例返回。避免来重复侦测value变化。而且,我们可以看到,在新建Observe实例时,vue.js是将实例挂载在value的_ob_属性上,所以细心的同学肯定也会发现我们平时开发时,一些响应式数据属性上总有一个_ob_属性,其实这就是Observe实例。

在拦截器中获取依赖

然后我们注意这一段代码:

this.dep = new Dep() //在此搜集依赖  保存在Observe
def(value, '_ob_', this) //将Observe挂载到 对象的_ob_属性上

这段代码将dep实例挂载这value上,然后将this设置在_ob_属性上,所以,我们后期就可以通过value的_ob_属性获取对应的依赖。

触发依赖

具体请查阅源码:/src/core/observer/array.js

/*
 * not type checking this file because flow doesn't play well with
 * dynamically accessing methods on Array prototype
 */

import { def } from '../util/index'

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})

在上面的代码中,通过使用ob.dep.notify()来通知依赖数据发生来改变。

侦测数组中元素的变化

前面提到的侦测数组变化,指的是数组本身的变化,比如:新增、删除一个元素。那么,如果数组中某一元素是对象,对象的属性值发生变化也是要侦测的。也就是说响应式数据的子项也是要被侦听的。所以我们需要递归的遍历数组,将数组中的每一项都设置成响应式的。

......
if (Array.isArray(value)) {
  dependArray(value);
}
......

 /**
   * Observe a list of Array items.
   */
  observeArray(items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i]);
    }
  }

侦测新增元素的变化

if (inserted) ob.observeArray(inserted)

Array的问题

前面说过,Vue.js对数组实现侦测的方式是在数组原型上进行监听的,所以有一些方法改变数组是Vue.js是无法监测到的,如:this.list.length = 0、this.list[0] = 2,这样并不会触发re-render或者watcher。

vue源码解读(二):变化侦测相关API的实现原理

vue源码解读(二):变化侦测相关API的实现原理

vm.$watch

用法

vm.$watch(expOrFn,callback,options)
参数:

  • { string | Function } expOrFn
  • { Function | Object } callback
  • { Object } options

返回值:{ Function } unwatch
用法:用于观察一个表达式或者computed函数在vue实例上的变化。回调函数调用时,会从参数中回去新数据和老数据。表达式直接受以.为分隔符的路径。
例如:

vm.$watch('a.b.c',function(newVal,oldVal){
    //todo
})

vm.$watch返回一个取消观察函数unwatch,用于停止触发回调。
参数options

  • deep:为了可以发现对象内部的属性变化,可以添加deep:true进行深度监听。
  • immediate:在参数中指定immediate:true,将立即以表达式的当前值触发回调函数。

原理

vm.$watch其实内部也是对Watcher的封装。我们来看一下vue内部是如何实现deepimmediate的。

src/core/instance/state.js

Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }

在这段代码中,可以看出,在初始化组件的时候,如果用户使用了immddiate参数,那么就会立即执行一次cb,返回一个新的函数unwatchFn(),在返回函数中调用了watcher的teardown()方法,我们来看一下该方法做了什么:

src/core/observer/watcher.js

/**
   * Remove self from all dependencies' subscriber list.
   */
  teardown () {
    if (this.active) {
      // remove self from vm's watcher list
      // this is a somewhat expensive operation so we skip it
      // if the vm is being destroyed.
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)
      }
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)
      }
      this.active = false
    }
  }

当执行该方法后,就会将Watcher实例从当前观察的依赖列表中移除。
因此,当前我们需要在Watcher中记录自己都订阅了谁,也就是Watcher实例都被收集进哪些Dep中了。然后当Watcher不想继续订阅这些依赖时,循环遍历这些记录,通知Dep,将自己从它们的依赖列表中移除。
现在Watcher中添加addDep方法,该方法的作用是让Watcher记录自己订阅过哪些Dep

src/core/observer/watcher.js

this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set() 
/**
   * Add a dependency to this directive.
   */
addDep (dep: Dep) {
  const id = dep.id
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id)
    this.newDeps.push(dep)
    if (!this.depIds.has(id)) {
      dep.addSub(this)
    }
  }
}

在上述代码中,通过depIds来判断当前Watcher是否订阅过该Dep,就可以避免重复订阅。
执行this.newDepIds.add(id)来记录当前Watcher一定订阅来该Dep
执行this.newDeps.push(dep)来记录自己订阅了哪些Dep
执行dep.addSub(this)来将自己订阅到Dep中。
现在我们已经知道了vue.$watcher是如何记录Dep的,上面也提到过它是通过循环遍历依赖列表,使用this.deps[i].removeSub(this)来移除依赖的,接下来我们来看看removeSub是如何实现移除依赖列表的:

src/core/observer/dep.js

removeSub (sub: Watcher) {
  remove(this.subs, sub)
}


/**
 * Remove an item from an array.
 */
export function remove (arr: Array<any>, item: any): Array<any> | void {
  if (arr.length) {
    const index = arr.indexOf(item)
    if (index > -1) {
      return arr.splice(index, 1)
    }
  }
}

其实就是使用数组的splice方法将其删除,然后当数据发生变化后,将不再通知该Watcher

deep的原理

在之前的一篇文章中讲过,Vue中的依赖收集和依赖触发。当Watcher想监听某个数据的时候,就会触发该数据的依赖收集逻辑,将自己收集进去,当数据发生变化的时候,通知Watcher。而实现deep功能,除了将当前数据进行依赖收集之外,还有递归的将其子数据也进行依赖收集,这样当子数据发生变化的时候,就会通知当前Watcher了。
具体实现:

src/core/observer/watcher.js

if (this.deep) {
  traverse(value)
}

src/core/observer/traverse.js

/* @flow */

import { _Set as Set, isObject } from '../util/index'
import type { SimpleSet } from '../util/index'
import VNode from '../vdom/vnode'

const seenObjects = new Set()

/**
 * Recursively traverse an object to evoke all converted
 * getters, so that every nested property inside the object
 * is collected as a "deep" dependency.
 */
export function traverse (val: any) {
  _traverse(val, seenObjects)
  seenObjects.clear()
}

function _traverse (val: any, seen: SimpleSet) {
  let i, keys
  const isA = Array.isArray(val)
  //如果当前数据不是数组、对象、或者被冻结、或者是VNode,直接返回
  if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
    return
  }
  if (val.__ob__) {
    //数据已经是响应式
    const depId = val.__ob__.dep.id
    //拿到depId,保证不会重复收集依赖
    if (seen.has(depId)) {
      return
    }
    seen.add(depId)
  }
  //如果是数组 循环遍历 将数组中的每一项都调用_traverse 收集依赖
  if (isA) {
    i = val.length
    while (i--) _traverse(val[i], seen)
  } else {
    //如果是对象 
    keys = Object.keys(val)
    i = keys.length
    while (i--) _traverse(val[keys[i]], seen)
  }
}

通过_traverse方法,就实现了对数组或者对象的深度监听。

vm.$set

用法

vm.$set(target,key,value)
参数:

  • { Object | Array } target
  • { string | number } key
  • { any } value

返回值:{ Function } unwatch
用法:在object上设置一个属性,如果object是响应式的,那么属性被创建之后也会是响应式的,被触发视图更新。该方法主要用来避开Vue不能侦测属性被添加的限制。

原理

src/core/observer/index.js

/**
 * Set a property on an object. Adds the new property and
 * triggers change notification if the property doesn't
 * already exist.
 */
export function set(target: Array<any> | Object, key: any, val: any): any {
  //如果是数组 则改变数组的length,使用splice方法在key后面添加一个value
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key);
    //使用splice方法,触发数组拦截器,触发依赖收集逻辑
    target.splice(key, 1, val);
    return val;
  }
  //如果是对象且key已经存在于对象中,则直接更改对象属性值
  if (key in target && !(key in Object.prototype)) {
    target[key] = val;
    return val;
  }
  //获取Observe实例 如果target已是响应式的 则ob存在
  const ob = (target: any).__ob__;
  //target不能是vue实例或者vue实例的根数据对象
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== "production" &&
      warn(
        "Avoid adding reactive properties to a Vue instance or its root $data " +
          "at runtime - declare it upfront in the data option."
      );
    return val;
  }
  //如果ob不存在 则target不是响应式对象 则不需要做特殊处理
  if (!ob) {
    target[key] = val;
    return val;
  }
  //否则 重新触发defineReactive,进行依赖收集
  defineReactive(ob.value, key, val);
  ob.dep.notify();
  return val;
}

这就是vm.$set()的原理。

vm.$delete

用法

vm.$delete(target,key)
参数:

  • { Object | Array } target
  • { string | number } key

用法:删除对象的属性如果对象是响应式的,则可以保证能触发更新视图。

原理

src/core/instance/state.js

import { del } from '../observer/index'
Vue.prototype.$delete = del;

src/core/observer/index.js

/**
 * Delete a property and trigger change if necessary.
 */
export function del(target: Array<any> | Object, key: any) {
  //target如果是数组 则使用splice触发依赖
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1);
    return;
  }
  const ob = (target: any).__ob__;
  //如果target是vue实例或者vue实例根数据对象
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== "production" &&
      warn(
        "Avoid deleting properties on a Vue instance or its root $data " +
          "- just set it to null."
      );
    return;
  }
  //如果target上不存在key属性
  if (!hasOwn(target, key)) {
    return;
  }
  //如果是对象 直接删除 
  delete target[key];
  //如果target不是响应式数据 不做任何处理
  if (!ob) {
    return;
  }
  //否则 触发依赖
  ob.dep.notify();
}

手写Promise A+

手写Promise A+

Promise 是异步编程的一种解决方案。ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。
所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
Promise对象有以下两个特点:

  • 对象的状态不受外界影响
  • 一旦状态改变,就不会再变,任何时候都可以得到这个结果。

基础实现Promise

function Promise(cb){
    let self = this;
  this.status = 'PENDING';//promise的状态 默认pending
  this.value = null;//promise的值
  this.onResolvedCallback = [];//成功的回调函数
  this.onRejectedCallback = [];//失败的回调函数
  function resolve(value){
          setTimeout(() => {
              self.value = value;
          //promise的状态变为resolved 之后,遍历调用成功回调函数
          self.onResolvedCallback.forEach(cb => {
                cb(self.value)
          })
      })
  }

  function reject(value){
          setTimeout(() => {
              self.value = value;
            //promise的状态变为rejected 之后,遍历调用成功回调函数
          self.onRejectedCallback.forEach(cb => {
                cb(self.value)
          })
      })
  }
}

在上面的代码中,我们建立一个Promise函数,在函数中添加了Promise的返回值value、成功的回调函数队列onResolvedCallback、失败的函数回调队列onRejectedCallback
然后编写了函数的resolvereject方法,在方法中使用setTimeout来实现异步。方法都接受一个参数,表示当Promise状态变为resolved、rejected时返回值。最后,遍历对应的回调队列,触发队列中的每一个回调函数,并且将value传入。

实现then

then方法的第一个参数是resolved状态的回调函数,第二个参数(可选)是rejected状态的回调函数。
then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。

Promise.prototype.then = function(onResolved,onRejected){
  //避免传入的不是函数
  onFulfilled =
        typeof onFulfilled === 'function' ? onFulfilled : value => value
    onRejected =
        typeof onRejected === 'function'
            ? onRejected
            : error => {
                    throw error
              }
  //缓存this上下文对象 在哪个promise中调用,指向谁
  let self = this;
    return new Promise((resolve,reject) => {
      if(self.status === 'RESOLVED'){//成功
        self.onResolvedCallback.push(function(){
        //调用成功回调 获取结果
          let result = onResolved(self.value);
        //如果返回值为新的promise对象 则继续调用then
        if(result instanceof Promise){
            result.then(resolve,reject);
        }else {
          //否则直接resolve
            resolve(result)
        }
      })
    }else if(self.status === 'REJECTED'){
        self.onRejectedCallback.push(function(){
        //调用回调 获取结果
          let result = onRejected(self.value);
        //如果返回值为新的promise对象 则继续调用then
        if(result instanceof Promise){
            result.then(resolve,reject);
        }else {
          //否则直接resolve
            reject(result)
        }
      })
    }else if (self.status === 'PENDING') {
            self.onResolvedCallback.push(function() {
                let result = onResolved(self.value)
                if (result instanceof Promise) {
                    result.then(resolve, reject)
                } else {
                    resolve(result)
                }
      })
      self.onRejectedCallback.push(function(){
        let result = onRejected(self.value);
        if(result instanceof Promise){
          result.then(resolve,reject)
        }else {
          reject(result)
        }
            })
        }
  })
}

then函数中,我们需要保存调用then函数时的上下文对象,然后根据此时promise的状态,分别进行不同的处理。

实现all

promise.all(),具有以下特性:

  1. 接收一个 `Promise_` 实例的数组或具有 `Iterator_`_ 接口的对象,_
  2. 如果元素不是 `Promise_` 对象,则使用 `Promise.resolve__ 转成 _Promise_` 对象_
  3. 如果全部成功,状态变为 `resolved_`_,返回值将组成一个数组传给回调
  4. 只要有一个失败,状态就变为 `rejected__,返回值将直接传递给回调__all() `的返回值也是新的 `Promise_` 对象_
Promisre.prototype.all = function(promises){
    return new Promise((resolve,reject) => {
      if(!Array.isArray(promises)){
        return reject(new TypeError('arguments must be an array'))
    }
    let count = 0,
        len=  promises.length,
        resolveValues = new Array(len);
    for (let i = 0; i < len; i++) {
      promises[i].then((value) => {
        count++;
        resolveValues[i] = value;
        if(count === len){
          return resolve(resolveValues)
        }
      },(value) => {
        return reject(value)
      })
    }
  })
}

ok,这样我们的promise A+基本就简单实现了。

CSS常见面试题

CSS常见面试题

  1. 介绍一下标准的CSS的盒子模型?与低版本IE的盒子模型有什么不同的?

标准盒子模型:宽度=内容的宽度(content)+ border + padding + margin
低版本IE盒子模型:宽度=内容宽度(content+border+padding)+ margin

  1. box-sizing属性

用来控制元素的盒子模型的解析模式,默认为content-box
context-box:W3C的标准盒子模型,设置元素的 height/width 属性指的是content部分的高/宽
border-box:IE传统盒子模型。设置元素的height/width属性指的是border + padding + content部分 的高/宽

  1. CSS选择器有哪些?哪些属性可以继承?CSS优先级算法如何计算?

CSS选择符:id选择器(#myid)、类选择器(.myclassname)、标签选择器(div, h1, p)、相邻选择器(h1 + p)、子选择器(ul > li)、后代选择器(li a)、通配符选择器(*)、属性选择器(a[rel=”external”])、伪类选择器(a:hover, li:nth-child)

可继承的属性:font-size, font-family, color
不可继承的样式:border, padding, margin, width, height
优先级(就近原则):!important > [ id > class > tag ]
!important 比内联优先级高

元素选择符: 1
class选择符: 10
id选择符:100
元素标签:1000

  1. !important声明的样式优先级最高,如果冲突再进行计算。
  2. 如果优先级相同,则选择最后出现的样式。
  3. 继承得到的样式的优先级最低。
  1. CSS3新增伪类有那些?

p:first-of-type 选择属于其父元素的首个元素
p:last-of-type 选择属于其父元素的最后元素
p:only-of-type 选择属于其父元素唯一的元素
p:only-child 选择属于其父元素的唯一子元素
p:nth-child(2) 选择属于其父元素的第二个子元素
:enabled :disabled 表单控件的禁用状态。
:checked 单选框或复选框被选中。

  1. 如何居中div?
<div class="main">
  <div class="content"></div>
</div>
.main{
  background-color: yellowgreen;
  width: 300px;
  height: 300px;
  text-align: center;
  vertical-align: middle;
  position: relative;
}
.content{
  background-color: red;
  width: 100px;
  height: 100px;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%,-50%);
} 
.content{
  background-color: red;
  width: 100px;
  height: 100px;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  margin: auto;
} 
.main{
  background-color: yellowgreen;
  width: 300px;
  height: 300px;
  text-align: center;
  vertical-align: middle;
  display: flex;
  align-items: center;
  justify-content: center;
}
  1. display有哪些值?说明他们的作用?
  • inline(默认)–内联
  • none–隐藏
  • block–块显示
  • table–表格显示
  • list-item–项目列表
  • inline-block
  1. position的值?
  • static(默认):按照正常文档流进行排列;
  • relative(相对定位):不脱离文档流,参考自身静态位置通过 top, bottom, left, right 定位;
  • absolute(绝对定位):参考距其最近一个不为static的父级元素通过top, bottom, left, right 定位;
  • fixed(固定定位):所固定的参照对像是可视窗口。
  1. 请解释一下CSS3的flexbox(弹性盒布局模型),以及适用场景?

该布局模型的目的是提供一种更加高效的方式来对容器中的条目进行布局、对齐和分配空间。在传统的布局方式中,block 布局是把块在垂直方向从上到下依次排列的;而 inline 布局则是在水平方向来排列。弹性盒布局并没有这样内在的方向限制,可以由开发人员自由操作。
试用场景:弹性布局适合于移动前端开发,在Android和ios上也完美支持。

  1. 用纯CSS创建一个三角形的原理是什么?
:root{
  --border-width:100px;
}
.main{
  width: 0;
  height: 0;
  margin-top: 100px;
}
.main1{
  border-left: 50px solid transparent;
  border-right: 50px solid transparent;
  border-bottom: var(--border-width) solid yellowgreen;
}
.main2{
  border-right: 100px solid yellowgreen;
  border-bottom: 50px solid transparent;
  border-top: 50px solid transparent;
}
.main3{
  border-right: 120px solid yellowgreen;
  border-bottom: 70px solid transparent;
  border-top: 30px solid transparent;
}
/**********************************************/
:root {
  --Width: 0;
  /* --Width:100px; */
  --border-width: 60px;
}

/*三角形 开口向左上*/
.main {
  width: var(--Width);
  height: var(--Width);
  border: var(--border-width) solid;
  border-color: yellow red green black;
  border-bottom-color: transparent;
  /*transparent 表示颜色透明*/
  border-right-color: transparent;
}

/*扇形 开口向下*/
.main2 {
  width: 0;
  height: 0;
  margin-top: 100px;
  border: 100px solid red;
  /* border-right-color: transparent;
            border-bottom-color: transparent;
            border-left-color: transparent; */
  border-color: #f00 transparent transparent;
  border-radius: 100px;
}

/*等腰梯形*/
.main3 {
  margin-top: 100px;
  width: 100px;
  height: 100px;
  border: 70px solid;
  border-color: red transparent transparent;
}

/*直角梯形*/
.main4 {
  margin-top: 100px;
  width: 0;
  height: 0;
  border: 70px solid;
  border-bottom-width: 0;
  border-color: red red transparent transparent;
}

/*平行四边形*/
.main5 {
  position: relative;
  margin-top: 100px;
  border: solid;
  width: 0;
  height: 0;
  border-width: 100px 70px;
  border-color: yellow red green black;
  border-right-color: transparent;
  border-left-color: transparent;
  border-top-color: transparent;
}

.main5::after {
  content: "";
  position: absolute;
  top: 0;
  right: -140px;
  border-right:70px solid transparent ;
  border-left:70px solid transparent ;
  border-top:100px solid red ;
}
  1. 常见的兼容性问题?
  • 同浏览器的标签默认的margin和padding不一样。*{margin:0;padding:0;}
  • IE6双边距bug:块属性标签float后,又有横行的margin情况下,在IE6显示margin比设置的大。hack:display:inline;将其转化为行内属性
  • 设置较小高度标签(一般小于10px),在IE6,IE7中高度超出自己设置高度。hack:给超出高度的标签设置overflow:hidden;或者设置行高line-height 小于你设置的高度。
  • Chrome 中文界面下默认会将小于 12px 的文本强制按照 12px 显示,可通过加入 CSS 属性 -webkit-text-size-adjust: none; 解决。

**

  1. display:none与visibility:hidden的区别?

display:none 不显示对应的元素,在文档布局中不再分配空间(回流+重绘)
visibility:hidden 隐藏对应元素,在文档布局中仍保留原来的空间(重绘)

  1. position跟display、overflow、float这些特性相互叠加后会怎么样?

display属性规定元素应该生成的框的类型;position属性规定元素的定位类型;float属性是一种布局方式,定义元素在哪个方向浮动。
类似于优先级机制:position:absolute/fixed优先级最高,有他们在时,float不起作用,display值需要调整。float 或者absolute定位的元素,只能是块元素或表格。

  1. 对BFC规范(块级格式化上下文:block formatting context)的理解?

BFC规定了内部的Block Box如何布局。
定位方案:

  1. 内部的Box会在垂直方向上一个接一个放置。
  2. Box垂直方向的距离由margin决定,属于同一个BFC的两个相邻Box的margin会发生重叠。
  3. 每个元素的margin box 的左边,与包含块border box的左边相接触。
  4. BFC的区域不会与float box重叠。
  5. BFC是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。
  6. 计算BFC的高度时,浮动元素也会参与计算。

满足下列条件之一就可触发BFC

  1. 根元素,即html
  2. float的值不为none(默认)
  3. overflow的值不为visible(默认)
  4. display的值为inline-block、table-cell、table-caption
  5. position的值为absolute或fixed

**

  1. 为什么会出现浮动和什么时候需要清除浮动?清除浮动的方式?

浮动元素碰到包含它的边框或者浮动元素的边框停留。由于浮动元素不在文档流中,所以文档流的块框表现得就像浮动框不存在一样。浮动元素会漂浮在文档流的块框上。
浮动带来的问题:

  1. 父元素的高度无法被撑开,影响与父元素同级的元素
  2. 与浮动元素同级的非浮动元素(内联元素)会跟随其后
  3. 若非第一个元素浮动,则该元素之前的元素也需要浮动,否则会影响页面显示的结构。

清除浮动的方式:

  1. 父级div定义height
  2. 最后一个浮动元素后加空div标签 并添加样式clear:both。
  3. 包含浮动元素的父标签添加样式overflow为hidden或auto。
  4. 父级div定义zoom
  5. 伪类(最佳)

最佳实现方法:

/*清除浮动,将该class写在浮动元素的父级元素或父级以上的 元素*/

.clearfix:after {
    content: ".";
    display: block;
    height: 0;
    font-size: 0;
    clear: both;
    visibility: hidden;
}


/*兼容ie*/

.clearfix {
    zoom: 1;
}

**

  1. CSS优化、提高性能的方法有哪些?
    1. 避免过度约束
    2. 避免后代选择符
    3. 避免链式选择符
    4. 使用紧凑的语法
    5. 避免不必要的命名空间
    6. 避免不必要的重复
    7. 最好使用表示语义的名字。一个好的类名应该是描述他是什么而不是像什么
    8. 避免!important,可以选择其他选择器
    9. 尽可能的精简规则,你可以合并不同类里的重复规则**
  2. 浏览器是怎样解析CSS选择器的?

CSS选择器的解析是从右向左解析的。若从左向右的匹配,发现不符合规则,需要进行回溯,会损失很多性能。若从右向左匹配,先找到所有的最右节点,对于每一个节点,向上寻找其父节点直到找到根元素或满足条件的匹配规则,则结束这个分支的遍历。两种匹配规则的性能差别很大,是因为从右向左的匹配在第一步就筛选掉了大量的不符合条件的最右节点(叶子节点),而从左向右的匹配规则的性能都浪费在了失败的查找上面。
而在 CSS 解析完毕后,需要将解析的结果与 DOM Tree 的内容一起进行分析建立一棵 Render Tree,最终用来进行绘图。在建立 Render Tree 时(WebKit 中的「Attachment」过程),浏览器就要为每个 DOM Tree 中的元素根据 CSS 的解析结果(Style Rules)来确定生成怎样的 Render Tree。

  1. margin和padding分别适合什么场景使用?

何时使用margin:

  1. 需要在border外侧添加空白
  2. 空白处不需要背景色
  3. 上下相连的两个盒子之间的空白,需要相互抵消时。

何时使用padding:

  1. 需要在border内侧添加空白
  2. 空白处需要背景颜色
  3. 上下相连的两个盒子的空白,希望为两者之和。

兼容性的问题:在IE5 IE6中,为float的盒子指定margin时,左侧的margin可能会变成两倍的宽度。通过改变padding或者指定盒子的display:inline解决。

  1. 全屏滚动的原理是什么?用到了CSS的哪些属性?

原理:有点类似于轮播,整体的元素一直排列下去,假设有5个需要展示的全屏页面,那么高度是500%,只是展示100%,剩下的可以通过transform进行y轴定位,也可以通过margin-top实现。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .wrap{
            width: 200px;
            overflow: hidden;
        }
        .mian{
            height: 200px;
            width: 1000px;
            overflow: hidden;
            animation: identifier 10s 1s linear infinite;
        }
        .mian div{
            width: 200px;
            height: 100%;
            float: left;
        }
        .box1{
            background-color: red;
        }
        .box2{
            background-color: yellow;
        }
        .box3{
            background-color: blue;
        }
        .box4{
            background-color: purple;
        }
        .box5{
            background-color: pink;
        }
        @keyframes identifier {
            0% {
                transform: translateX(0);
            }
            20% {
                transform: translateX(-200px);
            }
            40% {
                transform: translateX(-400px);
            }
            60% {
                transform: translateX(-600px);
            }
            80% {
                transform: translateX(-800px);
            }
            100% {
                transform: translateX(-0);
            }
        }
    </style>
</head>
<body>
   <div class="wrap">
        <div class="mian">
            <div class="box1">1</div>
            <div class="box2">2</div>
            <div class="box3">3</div>
            <div class="box4">4</div>
            <div class="box5">5</div>
        </div>
   </div>
</body>
</html>
  1. ::before 和 :after中双冒号和单冒号有什么区别和作用?
    1. 冒号(:)用于CSS3伪类,双冒号(::)用于CSS3伪元素。
    2. ::before就是以一个子元素的存在,定义在元素主体内容之前的一个伪元素。并不存在于dom之中,只存在在页面之中。

:before 和 :after 这两个伪元素,是在CSS2.1里新出现的。起初,伪元素的前缀使用的是单冒号语法,但随着Web的进化,在CSS3的规范里,伪元素的语法被修改成使用双冒号,成为::before ::after
**

  1. 你对line-height是如何理解的?

行高是指一行文字的高度,具体说是两行文字间基线的距离。CSS中起高度作用的是height和line-height,没有定义height属性,最终其表现作用一定是line-height。
单行文本垂直居中:把line-height值设置为height一样大小的值可以实现单行文字的垂直居中,其实也可以把height删除。
多行文本垂直居中:需要设置display属性为inline-block。

  1. 怎么让Chrome支持小于12px 的文字?

font-size: 20px;-webkit-transform: scale(0.8);

  1. 让页面里的字体变清晰,变细用CSS怎么做?

-webkit-font-smoothing:antialiased

  1. 如果需要手动写动画,你认为最小时间间隔是多久,为什么?

多数显示器默认频率是60Hz,即1秒刷新60次,所以理论上最小间隔为1/60*1000ms = 16.7ms。

  1. li与li之间有看不见的空白间隔是什么原因引起的?有什么解决办法?

行框的排列会受到中间空白(回车空格)等的影响,因为空格也属于字符,这些空白也会被应用样式,占据空间,所以会有间隔,把字符大小设为0,就没有空格了。
解决方法:

  1. 可以将

  2. 代码全部写在一排

  3. 浮动li中float:left

  4. 在ul中用font-size:0(谷歌不支持);可以使用letter-space:-3px

**

  1. png、jpg、gif 这些图片格式解释一下,分别什么时候用。有没有了解过webp?

png:
特点:支持无损压缩、体积大、质量好、支持透明
使用场景:png在处理线条和颜色对比方面有优势,所以经常会被用来做logo或者颜色简单且对比强烈的背景图
jpg/jpeg:
特点:有损压缩、体积小、加载快、不支持透明
使用场景:适用于颜色丰富的场景、如背景图、轮播图、banner图等。
svg:
特点:文本文件、体积小、不失真、兼容性好
使用场景:将 SVG 写入独立文件后引入 HTML
base64 :
特点: 文本文件、基于编码、小图标解决方案
使用场景:小图标
webp:

WebP 是 Google 专为 Web 开发的一种旨在加快图片加载速度的图片格式,它支持有损压缩和无损压缩

特点:兼容png和jpg的优点,但是有兼容性问题。

  1. CSS属性overflow属性定义溢出元素内容区的内容会如何处理?
  • 参数是scroll时候,必会出现滚动条。
  • 参数是auto时候,子元素内容大于父元素时出现滚动条。
  • 参数是visible时候,溢出的内容出现在父元素之外。
  • 参数是hidden时候,溢出隐藏。
  1. 阐述一下CSS Sprites

将一个页面涉及到的所有图片都包含到一张大图中去,然后利用CSS的 background-image,background- repeat,background-position 的组合进行背景定位。利用CSS Sprites能很好地减少网页的http请求,从而大大的提高页面的性能;CSS Sprites能减少图片的字节。

  1. div垂直居中 左右10px 宽为高的2倍 内容垂直居中

关键点:padding-top、padding-bottom、margin-top、margin-bottom为百分比的时候,都是相对于父元素的width。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>div垂直居中,左右10px,高度始终为宽度一半</title>
    <style>
        * {
            margin: 0;
            padding: 0;
        }

        html,
        body {
            height: 100%;
            width: 100%;
        }

        .outer-wrap {
            height: 100%;
            margin: 0 10px;
            background-color: yellowgreen;
            display: flex;
            align-items: center;
        }

        .inner_wrapper {
            background: red;
            position: relative;
            width: 100%;
            height: 0;
            padding-bottom: 50%;
        }

        .text {
            width: 100%;
            height: 100%;
            position: absolute;
            display: flex;
            align-items: center;
            justify-content: center;
        }
    </style>
</head>

<body>
    <div class="outer-wrap">
        <div class="inner_wrapper">
            <div class="text">A</div>
        </div>
    </div>
</body>

</html>

第二种:

.wrapper {
  position: relative;
  width: 100%;
  height: 100%;
}
.box{
  margin-left: 10px;
  width: calc(100vw - 20px);
  height: calc(50vw - 10px);
  background-color: yellowgreen;
  display: flex;
  align-items: center;
  justify-content: center;
}
  1. **

webpack面试题集锦

webpack面试集锦

Webpack构建流程简单说一下

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

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

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

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

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

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

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

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

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

那你再说一说Loader和Plugin的区别?

Loader 本质就是一个函数,在该函数中对接收到的内容进行转换,返回转换后的结果。
因为 Webpack 只认识 JavaScript,所以 Loader 就成了翻译官,对其他类型的资源进行转译的预处理工作。
Plugin 就是插件,基于事件流框架 Tapable,插件可以扩展 Webpack 的功能,在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
Loader 在 module.rules 中配置,作为模块的解析规则,类型为数组。每一项都是一个 Object,内部包含了 test(类型文件)、loader、options (参数)等属性。
Plugin 在 plugins 中单独配置,类型为数组,每一项是一个 Plugin 的实例,参数都通过构造函数传入。

source map是什么?生产环境怎么用?

**source map 是将编译、打包、压缩后的代码映射回源代码的过程。打包压缩后的代码不具备良好的可读性,想要调试源码就需要 soucre map。
map文件只要不打开开发者工具,浏览器是不会加载的。
线上环境一般有三种处理方案:

  • hidden-source-map:借助第三方错误监控平台 Sentry 使用

  • nosources-source-map:只会显示具体行数以及查看源代码的错误栈。安全性比 sourcemap 高

  • sourcemap:通过 nginx 设置将 .map 文件只对白名单开放(公司内网)

注意:避免在生产中使用 inline-eval-,因为它们会增加 bundle 体积大小,并降低整体性能。
**

文件监听原理呢?

在发现源码发生变化时,自动重新构建出新的输出文件。
Webpack开启监听模式,有两种方式:

  • 启动 webpack 命令时,带上 –watch 参数
  • 在配置 webpack.config.js 中设置 watch:true

缺点:每次需要手动刷新浏览器
原理:轮询判断文件的最后编辑时间是否变化,如果某个文件发生了变化,并不会立刻告诉监听者,而是先缓存起来,等 aggregateTimeout 后再执行

module.export = {    
  // 默认false,也就是不开启    
  watch: true,    
  // 只有开启监听模式时,watchOptions才有意义    
  watchOptions: {        
    // 默认为空,不监听的文件或者文件夹,支持正则匹配        
    ignored: /node_modules/,        
    // 监听到变化发生后会等300ms再去执行,默认300ms        
    aggregateTimeout:300,       
    // 判断文件是否发生变化是通过不停询问系统指定文件有没有变化实现的,默认每秒问1000次        
    poll:1000    
  }
}

使用webpack开发时,你用过哪些可以提高效率的插件?

  • webpack-dashboard:可以更友好的展示相关打包信息。

  • webpack-merge:提取公共配置,减少重复配置代码

  • speed-measure-webpack-plugin:简称 SMP,分析出 Webpack 打包过程中 Loader 和 Plugin 的耗时,有助于找到构建过程中的性能瓶颈。

  • size-plugin:监控资源体积变化,尽早发现问题

  • HotModuleReplacementPlugin:模块热替换

Webpack 的热更新原理

Webpack 的热更新又称热替换(Hot Module Replacement),缩写为 HMR。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。
image.png
上图是webpack 配合 webpack-dev-server 进行应用开发的模块热更新流程图。

  • 上图底部红色框内是服务端,而上面的橙色框是浏览器端。
  • 绿色的方框是 webpack 代码控制的区域。蓝色方框是 webpack-dev-server 代码控制的区域,洋红色的方框是文件系统,文件修改后的变化就发生在这,而青色的方框是应用本身。

上图显示了我们修改代码到模块热更新完成的一个周期,通过深绿色的阿拉伯数字符号已经将 HMR 的整个过程标识了出来。

  1. 第一步,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中。
  2. 第二步是 webpack-dev-server 和 webpack 之间的接口交互,而在这一步,主要是 dev-server 的中间件 webpack-dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调用 webpack 暴露的 API对代码变化进行监控,并且告诉 webpack,将代码打包到内存中。
  3. 第三步是 webpack-dev-server 对文件变化的一个监控,这一步不同于第一步,并不是监控代码变化重新打包。当我们在配置文件中配置了devServer.watchContentBase 为 true 的时候,Server 会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload。注意,这儿是浏览器刷新,和 HMR 是两个概念。
  4. 第四步也是 webpack-dev-server 代码的工作,该步骤主要是通过 sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也包括第三步中 Server 监听静态文件变化的信息。浏览器端根据这些 socket 消息进行不同的操作。当然服务端传递的最主要信息还是新模块的 hash 值,后面的步骤根据这一 hash 值来进行模块热替换。
  5. webpack-dev-server/client 端并不能够请求更新的代码,也不会执行热更模块操作,而把这些工作又交回给了 webpack,webpack/hot/dev-server 的工作就是根据 webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,也就没有后面那些步骤了。
  6. HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接收到上一步传递给他的新模块的 hash 值,它通过 JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端返回一个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块代码。这就是上图中 7、8、9 步骤。
  7. 而第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,HotModulePlugin 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。
  8. 最后一步,当 HMR 失败后,回退到 live reload 操作,也就是进行浏览器刷新来获取最新打包代码。

参考:https://zhuanlan.zhihu.com/p/30669007
HMR的核心就是客户端从服务端拉去更新后的文件,准确的说是 chunk diff (chunk 需要更新的部分),实际上 webpack-dev-server(WDS) 与浏览器之间维护了一个 Websocket,当本地资源发生变化时,WDS 会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比。客户端对比出差异后会向 WDS 发起 Ajax 请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 WDS 发起 jsonp 请求获取该chunk的增量更新。

如何对bundle体积进行监控和分析?

VSCode 中有一个插件 Import Cost 可以帮助我们对引入模块的大小进行实时监测,还可以使用 webpack-bundle-analyzer 生成 bundle 的模块组成图,显示所占体积。

文件指纹是什么?怎么用?

文件指纹是打包后输出的文件名的后缀。

  • Hash:和整个项目的构建相关,只要项目文件有修改,整个项目构建的 hash 值就会更改

  • Chunkhash:和 Webpack 打包的 chunk 有关,不同的 entry 会生出不同的 chunkhash

  • Contenthash:根据文件内容来定义 hash,文件内容不变,则 contenthash 不变

js文件指纹设置

module.exports = {    
      entry: {        
      app: './scr/app.js'    
    },    
  output: {       
          filename: '[name][chunkhash:8].js',        
        path:__dirname + '/dist'    
  }
}

css设置文件指纹

plugins:[
  new MiniCssExtractPlugin({
    filename: `[name][contenthash:8].css`
  })
]

图片的文件指纹
设置file-loader的name,使用hash。
占位符名称及含义

  • ext 资源后缀名
  • name 文件名称
  • path 文件的相对路径
  • folder 文件所在的文件夹
  • contenthash 文件的内容hash,默认是md5生成
  • hash 文件内容的hash,默认是md5生成
  • emoji 一个随机的指代文件内容的emoj
module:{     
  rules:[
    {            
      test:/\.(png|svg|jpg|gif)$/,            
      use:[{                
        loader:'file-loader',                
        options:{                    
          name:'img/[name][hash:8].[ext]'               
        }            
      }]        
    }
  ]    
}

如何优化 Webpack 的构建速度?

  • 使用高版本的 Webpack 和 Node.js

  • 多进程/多实例构建:HappyPack(不维护了)、thread-loader

  • 压缩代码

    • 多进程并行压缩
      • webpack-paralle-uglify-plugin
      • uglifyjs-webpack-plugin 开启 parallel 参数 (不支持ES6)
      • terser-webpack-plugin 开启 parallel 参数
    • 通过 mini-css-extract-plugin 提取 Chunk 中的 CSS 代码到单独文件,通过 css-loader 的 minimize 选项开启 cssnano 压缩 CSS。
  • 图片压缩

    • 使用基于 Node 库的 imagemin (很多定制选项、可以处理多种图片格式)
    • 配置 image-webpack-loader
  • 缩小打包作用域

    • exclude/include (确定 loader 规则范围)
    • resolve.modules 指明第三方模块的绝对路径 (减少不必要的查找)
    • resolve.mainFields 只采用 main 字段作为入口文件描述字段 (减少搜索步骤,需要考虑到所有运行时依赖的第三方模块的入口文件描述字段)
    • resolve.extensions 尽可能减少后缀尝试的可能性
    • noParse 对完全不需要解析的库进行忽略 (不去解析但仍会打包到 bundle 中,注意被忽略掉的文件里不应该包含 import、require、define 等模块化语句)
    • IgnorePlugin (完全排除模块)
    • 合理使用alias
  • 提取页面公共资源

    • 基础包分离:
      • 使用 html-webpack-externals-plugin,将基础包通过 CDN 引入,不打入 bundle 中
      • 使用 SplitChunksPlugin 进行(公共脚本、基础包、页面公共文件)分离(Webpack4内置) ,替代了 CommonsChunkPlugin 插件
  • DLL

    • 使用 DllPlugin 进行分包,使用 DllReferencePlugin(索引链接) 对 manifest.json 引用,让一些基本不会改动的代码先打包成静态资源,避免反复编译浪费时间。
    • HashedModuleIdsPlugin 可以解决模块数字id问题
  • 充分利用缓存提升二次构建速度

    • babel-loader 开启缓存
    • terser-webpack-plugin 开启缓存
    • 使用 cache-loader 或者 hard-source-webpack-plugin
  • Tree shaking

    • 打包过程中检测工程中没有引用过的模块并进行标记,在资源压缩时将它们从最终的bundle中去掉(只能对ES6 Modlue生效) 开发中尽可能使用ES6 Module的模块,提高tree shaking效率
    • 禁用 babel-loader 的模块依赖解析,否则 Webpack 接收到的就都是转换过的 CommonJS 形式的模块,无法进行 tree-shaking
    • 使用 PurifyCSS(不在维护) 或者 uncss 去除无用 CSS 代码
      • purgecss-webpack-plugin 和 mini-css-extract-plugin配合使用(建议)
  • Scope hoisting

    • 构建后的代码会存在大量闭包,造成体积增大,运行代码时创建的函数作用域变多,内存开销变大。Scope hoisting 将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突
    • 必须是ES6的语法,因为有很多第三方库仍采用 CommonJS 语法,为了充分发挥 Scope hoisting 的作用,需要配置 mainFields 对第三方模块优先采用 jsnext:main 中指向的ES6模块化语法
  • 动态Polyfill

    • 建议采用 polyfill-service 只给用户返回需要的polyfill,社区维护。 (部分国内奇葩浏览器UA可能无法识别,但可以降级返回所需全部polyfill)

Babel原理

大多数JavaScript Parser遵循 estree 规范,Babel 最初基于 acorn 项目(轻量级现代 JavaScript 解析器)
Babel大概分为三大部分:

  • 解析:将代码转换成 AST
    • 词法分析:将代码(字符串)分割为token流,即语法单元成的数组
    • 语法分析:分析token流(上面生成的数组)并生成 AST
  • 转换:访问 AST 的节点进行变换操作生产新的 AST
  • 生成:以新的 AST 为基础生成代码

想了解如何一步一步实现一个编译器的同学可以移步 Babel 官网曾经推荐的开源项目
the-super-tiny-compiler