【面试题库】源码系列 + 设计模式

214 阅读6分钟

目录:

  1. diff双指针问题
  2. 有几种设计模式?需要结合实际的使用场景去理解。
  3. 观察者模式 和 订阅发布模式 的区别
  4. diff算法详细说下
  5. vue是如何异步更新的?多长时间更新一次?
  6. vue的渲染流程
  7. 装饰器模式
  8. 发布订阅模式
  9. 代理模式
  10. 策略模式
  11. 单例模式

1,diff双指针问题

2,有几种设计模式?需要结合实际的使用场景去理解。

3,观察者模式 和 订阅发布模式 的区别

1) 订阅发布模式

image.png

浏览器的事件监听就是观察者模式。

5,diff算法详细说下

参考回答:

当组件创建和更新时,vue均会执行内部的update函数,该函数使用render函数生成的虚拟dom树,将新旧两树进行对比,找到差异点,最终更新到真实dom

对比差异的过程叫diff,vue在内部通过一个叫patch的函数完成该过程

在对比时,vue采用深度优先、同层比较的方式进行比对

在判断两个节点是否相同时,vue是通过虚拟节点的key和tag来进行判断的

具体来说,首先对根节点进行对比,如果相同则将旧节点关联的真实dom的挂到新节点上,然后根据需要更新属性到真实dom,然后再对比其子节点数组;如果不相同,则按照新节点的信息递归创建所有真实dom,同时挂到对应虚拟节点上,然后移除掉旧的dom。

对比其子节点数组时,vue对每个子节点数组使用了两个指针,分别指向头尾,然后不断向中间靠拢来进行对比,这样做的目的是尽量复用真实dom,尽量少的销毁和创建真实dom。如果发现相同,则进入和根节点一样的对比流程,如果发现不同,则移动真实dom到合适的位置。

这样一直递归的遍历下去,直到整棵树完成对比。

6,vue是如何异步更新的?多长时间更新一次?

比如你修改了数据,这边不会直接更新dom,而是会放入到任务队列当中, 这里还有一个更新的降级策略。比如,你在一个循环当中修改了1000次a的值,那么,vue是只会更新最后一次到dom。

7,vue的渲染流程

从代码出发,一段模版,首先是编译成ast, 然后对ast优化比如增加静态节点标记, 然后转换为render渲染函数 渲染函数再生成循环dom树vnode 然后就是新旧dom树diff,然后更新真实dom

8,装饰器模式

定义: 装饰者模式能够在不改变对象自身的基础上,在运行程序期间给对象动态地添加职责。

使用场景: 类似于拦截器,添加对象的前置和后置事件等。

Function.prototype.before = function(beforefn) {
    let _self = this;                          
    return function () {
        beforefn.apply(this, arguments);
        return _self.apply(this, arguments);
    }
}
Function.prototype.after = function(afterfn) {
    let _self = this;
    return function(){
        let ret = _self.apply(this, arguments);
        afterfn.apply(this, arguments);
        return ret;
    }
}
let func = function() {
    console.log('2');
}
//func1和func3为挂载函数
let func1 = function() {
    console.log('1');
}
let func3 = function() {
    console.log('3');
}

func = func.before(func1).after(func3);
func();   // 1  2  3


9、 发布订阅模式

**定义:**订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Event Channel),当发布者(Publisher)发布该事件(Publish Event)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。

**使用场景:**微信公众号的订阅

let eventEmitter = {
    list: {}, 

    on(event, fn) {
        // 订阅
        let _this = this;
        _this.list[event] = _this.list[event] || [];
        _this.list[event].push(fn);
        return _this;
    },

    emit() {
        // 发布
        let _this = this;
        let event = [].shift.call(arguments),
            fns = _this.list[event];
        if (fns && fns.length) {
            fns.forEach((fn) => fn.apply(_this, arguments));
        }
        return _this;
    },

    off(event, fn) {
        // 取消订阅
        let _this = this;
        let fns = _this.list[event];
        if (!fns) return false;
        if (!fn) {
            fns.length = 0;
        } else {
            for (let i = 0; i < fns.length; i++) {
                if (fns[i] === fn || fns[i].fn === fn) {
                    fns.splice(i, 1);
                    break;
                }
            }
        }
    }
};

const user1 = (content) => {
    console.log("用户1订阅了:", content);
};

const user2 = (content) => {
    console.log("用户2订阅了:", content);
};

const user3 = (content) => {
    console.log("用户3订阅了:", content);
};

// 订阅
eventEmitter.on("article1", user1);
eventEmitter.on("article1", user2);
eventEmitter.on("article2", user3);

eventEmitter.emit("article1", "Javascript 发布-订阅模式");
eventEmitter.emit("article2", "Javascript 观察者模式");

eventEmitter.off("article1", user1);
eventEmitter.emit("article1", "Javascript 发布-订阅模式");

//用户1订阅了: Javascript 发布-订阅模式
//用户2订阅了: Javascript 发布-订阅模式
//用户3订阅了: Javascript 观察者模式
//用户2订阅了: Javascript 发布-订阅模式


10、 代理模式

定义: 为一个对象提供一个代用品或占位符,以便控制对它的访问。

使用场景: 比如图片懒加载,先缓存动态 loading,必要时传入 src。

const imgFunc = (() => {
    let imgNode = document.createElement('img');
    document.body.appendChild(imgNode);
    return {
        setSrc: (src) => {
            imgNode.src = src;
        }
    }
})();

const ProxyImg = (() => {
    let img = new Image();
    img.onload = () => {
        let node = document.getElementsByTagName('img')
        imgFunc.setSrc(img.src);
    }
    return {
        setSrc: (src) => {
            imgFunc.setSrc('../C3photo/jacky/1.jpg');
            img.src = src;
        }
    }
})();

ProxyImg.setSrc('../C3photo/jacky/2.jpg');

11、 策略模式

定义: 定义一个策略类只专注与各方法算法实现,定义一个接口调用这些方法。

特点:代码优雅,可读性高。

// 策略类
const levelObj = {
    "A": money => money * 4,
    "B": money => money * 3,
    "C": money => money * 2
}

// 环境类  封装调用接口
const getMoney = (level, money) => levelObj[level](money);

console.log(getMoney('A', 200))   // 800

12、 单例模式

*定义:**一个类只返回一个实例,一旦创建再次调用就直接返回。

**使用场景:**比如自定义弹窗,无论你程序中多少调用,都只应创建一个弹窗对象。

class CreateUser {
    constructor(name) {
        this.name = name;
        this.getName();
    }
    getName() {
        return this.name;
    }
};

const ProxyMode = (() => {
    let instance = null;
    return (name) => {
        if(!instance) {
            instance = new CreateUser(name);
        }
        return instance;
    }
})();

let a = ProxyMode('obj1');
let b = ProxyMode('obj2');

console.log(a, b);   //  obj1 CreateUser {name: "obj1"}  obj1 CreateUser {name: "obj1"}单例模式只会创建一次实例
let c = ProxyMode('obj3');
console.log(c);   //  obj1  CreateUser {name: "obj1"}

参考

diff 总结

  • vue采用深度优先、同层比较的方式进行比对
  • 在判断两个节点是否相同时,vue是通过虚拟节点的key和tag来进行判断的
  • 对比其子节点数组时,vue对每个子节点数组使用了两个指针,分别指向头尾,然后不断向中间靠拢来进行对比,这样做的目的是尽量复用真实dom,尽量少的销毁和创建真实dom。如果发现相同,则进入和根节点一样的对比流程,如果发现不同,则移动真实dom到合适的位置。

总结

  • 装饰者模式 能够在不改变对象自身的基础上,在运行程序期间给对象动态地添加职责

  • 代理模式 为一个对象提供一个代用品或占位符以便控制对它的访问比如图片懒加载,先缓存动态 loading,必要时传入 src

  • 单例模式 一个类只返回一个实例,一旦创建再次调用就直接返回。 使用场景: 自定义弹窗,无论你程序中多少调用,都只应创建一个弹窗对象