框架相关手写题

1 将虚拟 Dom 转化为真实 Dom

{
  tag: 'DIV',
  attrs:{
  id:'app'
  },
  children: [
    {
      tag: 'SPAN',
      children: [
        { tag: 'A', children: [] }
      ]
    },
    {
      tag: 'SPAN',
      children: [
        { tag: 'A', children: [] },
        { tag: 'A', children: [] }
      ]
    }
  ]
}

把上面虚拟Dom转化成下方真实Dom

  function createDOM(vnode) {

            if (typeof vnode === 'string') {
                // 用于创建文本节点,即将一个字符串转换为一个 DOM 元素
                return document.createTextNode(vnode);
            }
            // 解构赋值
            const {
                tag,
                attrs = {},
                children = []
            } = vnode;

            const el = document.createElement(tag);
            // 将其作为元素的属性名
            for (let attr in attrs) {
                el.setAttribute(attr, attrs[attr]);
            }

            for (let child of children) {
                el.appendChild(createDOM(child));
            }

            return el;
        }

2.实现事件总线结合Vue应用

全局事件总线,严格来说不能说是观察者模式,而是发布-订阅模式。它在我们日常的业务开发中应用非常广。

在Vue中使用Event Bus来实现组件间的通讯

Event Bus/Event Emitter 作为全局事件总线,它起到的是一个沟通桥梁的作用。我们可以把它理解为一个事件中心,我们所有事件的订阅/发布都不能由订阅方和发布方“私下沟通”,必须要委托这个事件中心帮我们实现。

在Vue中,有时候 A 组件和 B 组件中间隔了很远,看似没什么关系,但我们希望它们之间能够通信。这种情况下除了求助于 Vuex 之外,我们还可以通过 Event Bus 来实现我们的需求。

创建一个 Event Bus(本质上也是 Vue 实例)并导出:

const EventBus = new Vue()
export default EventBus
在主文件里引入EventBus,并挂载到全局:
import bus from 'EventBus的文件路径'
Vue.prototype.bus = bus

订阅事件:

// 这里func指someEvent这个事件的监听函数
this.bus.$on('someEvent', func)

发布(触发)事件:

// 这里params指someEvent这个事件被触发时回调函数接收的入参
this.bus.$emit('someEvent', params)
大家会发现,整个调用过程中,没有出现具体的发布者和订阅者(比如上面的PrdPublisher
DeveloperObserver),全程只有bus这个东西一个人在疯狂刷存在感。这就是全局事件总线的特点——所有事件的发布/订阅操作,必须经由事件中心,禁止一切“私下交易”!
 class EventEmitter {
            constructor() {
                // handlers是一个map,用于存储事件与回调之间的对应关系
                this.handlers = {}
            }
            // on方法用于安装事件监听器,它接受目标事件名和回调函数作为参数
            on(eventName, cb) {
                // 先检查一下目标事件名有没有对应的监听函数队列 
                if (!this.handlers[eventName]) {
                    // 如果没有,那么首先初始化一个监听函数队列
                    this.handlers[eventName] = []
                }
                // 把回调函数推入目标事件的监听函数队列里去
                this.handlers[eventName].push(cb)
            }
            // emit方法用于触发目标事件,它接受事件名和监听函数入参作为参数
            emit(eventName, ...args) {
                // 检查目标事件是否有监听函数队列
                if (this.handlers[eventName]) {
                    this.handlers[eventName].forEach((callback) => {
                        callback(...args)
                    })
                }
            }
            // 移除某个事件回调队列里的指定回调函数
            off(eventName, cb) {
                const callbacks = this.handlers[eventName];
                const index = callbacks.indexOf(cb);
                if (index !== -1) {
                    callbacks.splice(index, 1)
                }
            }
            // 为事件注册单次监听器
            once(eventName, cb) {
                // 对回调函数进行包装,使其执行完毕自动被移除
                const wrapper = (...args) => {
                    cb.apply(...args);
                    this.off(eventName, wrapper)
                }
                this.on(eventName, wrapper)
            }
        }

3 实现一个双向绑定

defineProperty 版本

  // 数据
        const data = {
            text: 'default'
        };
        const input = document.getElementById('input');
        const span = document.getElementById('span');
        // 数据劫持
        Object.defineProperty(data, 'text', {
            // 数据变化 --> 修改视图
            set(newVal) {
                input.value = newVal;
                span.innerHTML = newVal;
            }
        })
        // 视图更改 --> 数据变化
        input.addEventListener('keyup', function (e) {
            data.text = e.target.value;
        });
proxy 版本
 // 数据
        const data = {
            text: 'default'
        };
        const input = document.getElementById('input');
        const span = document.getElementById('span');
        // 数据劫持
        const handler = {
            set(target, key, value) {
                target[key] = value;
                // 数据变化 --> 修改视图
                input.value = value;
                span.innerHTML = value;
                return value;
            }
        }
        const proxy = new Proxy(data, handler);
        // 视图更改 --> 数据变化
        input.addEventListener('keyup', function (e) {
            proxy.text = e.target.value;
        });

4 实现一个简易的MVVM

  1. 首先我会定义一个类Vue,这个类接收的是一个options,那么其中可能有需要挂载的根元素的id,也就是el属性;然后应该还有一个data属性,表示需要双向绑定的数据。
  2. 其次我会定义一个Dep类,这个类产生的实例对象中会定义一个subs数组用来存放所依赖这个属性的依赖,已经添加依赖的方法addSub,还有一个update方法用来遍历更新它subs中的所有依赖,同时Dep类有一个静态属性target它用来表示当前的观察者,当后续进行依赖收集的时候可以将它添加到dep.subs中。
  3. 然后设计一个observe方法,这个方法接收的是传进来的data,也就是options.data,里面会遍历data中的每一个属性,并使用Object.defineProperty()来重写它的getset,那么这里面呢可以使用new Dep()实例化一个dep对象,在get的时候调用其addSub方法添加当前的观察者Dep.target完成依赖收集,并且在set的时候调用dep.update方法来通知每一个依赖它的观察者进行更新。
  4. 完成这些之后,我们还需要一个compile方法来将HTML模版和数据结合起来。在这个方法中首先传入的是一个node节点,然后遍历它的所有子级,判断是否有firstElmentChild,有的话则进行递归调用compile方法,没有firstElementChild的话且该child.innderHTML用正则匹配满足有/\{\{(.*)\}\}/项的话则表示有需要双向绑定的数据,那么就将用正则new Reg('\\{\\{\\s*' + key + '\\s*\\}\\}', 'gm')替换掉是其为msg变量。
  5. 完成变量替换的同时,还需要将Dep.target指向当前的这个child,且调用一下this.opt.data[key],也就是为了触发这个数据的get来对当前的child进行依赖收集,这样下次数据变化的时候就能通知child进行视图更新了,不过在最后要记得将Dep.target指为null哦(其实在Vue中是有一个targetStack栈用来存放target的指向的)。
  6. 那么最后我们只需要监听documentDOMContentLoaded然后在回调函数中实例化这个Vue对象就可以了

姓名

{{name}}

年龄

{{age}}

 document.addEventListener("DOMContentLoaded", () => {
        let opt = {
            el: "#app",
            data: {
                name: "等待修改...",
                age: 20
            }
        };
        let vm = new Vue(opt);
        setTimeout(() => {
            opt.data.name = "jing";
        }, 2000);
    }, false)

    class Vue {
        constructor(opt) {
            this.opt = opt;
            this.observer(opt.data);
            let root = document.querySelector(opt.el);
            this.compile(root);
        }
        observer(data) {
            // 遍历数据对象
            Object.keys(data).forEach((key) => {
                // 创建一个 Dep 对象实例
                let obv = new Dep();
                // 为每一个属性添加一个下划线开头的备份属性
                data["_" + key] = data[key];
                // 通过 Object.defineProperty() 方法为数据对象的每一个属性设置 getter 和 setter
                Object.defineProperty(data, key, {
                    // getter 方法,用于获取属性值
                    get() {
                        // 在 Dep.target 存在的情况下,向当前 Dep 对象添加订阅者(即存储一个对该订阅者的引用)
                        Dep.target && obv.addSubNode(Dep.target);
                        // 返回备份属性的值
                        return data["_" + key];
                    },
                    set(newVal) {
                        // 通过当前 Dep 对象向所有订阅该对象的订阅者发送通知
                        obv.update(newVal);
                        // 更新备份属性的值
                        data["_" + key] = newVal;
                    }
                })
            })
        }
        compile(node) {
            // 通过 Array.prototype.forEach.call 将 NodeList 转换为数组并循环处理
            [].forEach.call(node.childNodes, (child) => {
                // 如果该节点没有子节点,且内部包含形如 {{xxx}} 的模板字符串,则执行以下逻辑
                if (!child.firstElementChild && /\{\{(.*)\}\}/.test(child.innerHTML)) {
                    // 获取模板字符串中的变量名
                    let key = RegExp.$1.trim();
                    // 将模板字符串中的变量名替换为变量的实际值
                    child.innerHTML = child.innerHTML.replace(
                        new RegExp("\\{\\{\\s*" + key + "\\s*\\}\\}", "gm"),
                        this.opt.data[key]
                    )
                    // 将当前节点设置为 Dep.target(订阅器的静态属性)
                    Dep.target = child;
                    // 获取变量的实际值,这会触发该变量的 getter,从而将当前节点添加为其依赖
                    this.opt.data[key];
                    // 将 Dep.target 重置为 null
                    Dep.target = null;
                } // 如果该节点有子节点,则递归调用 compile 函数处理子节点
                else if (child.firstElementChild) {
                    this.compile(child);
                }
            })
        }
    }
    class Dep {
        constructor() {
            this.subNode = [];
        }
        // 添加一个新的节点到subNode数组中
        addSubNode(node) {
            this.subNode.push(node);
        }
        // 遍历subNode数组,更新节点的内容为newVal
        update(newVal) {
            this.subNode.forEach((node) => {
                node.innerHTML = newVal;
            });
        }
    }

简化版2

function update() {
        console.log('数据变化~~~ mock update view')
    }
    let obj = [1, 2, 3];
    // 变异方法 push shift unshfit reverse sort splice pop
    // Object.defineProperty
    let oldProto = Array.prototype;
    let proto = Object.create(oldProto); // 克隆了一分
    ['push', 'shift'].forEach(item => {
        proto[item] = function () {
            update();
            oldProto[item].apply(this, arguments);
        }
    })

    function observer(value) { // proxy reflect
        if (Array.isArray(value)) {
            return value.__proto__ = proto;
            // 重写 这个数组里的push shift unshfit reverse sort splice pop
        }
        if (typeof value !== 'object') {
            return value;
        }
        for (let key in value) {
            defineReactive(value, key, value[key]);
        }
    }

    function defineReactive(obj, key, value) {
        observer(value); // 如果是对象 继续增加getter和setter
        Object.defineProperty(obj, key, {
            get() {
                return value;
            },
            set(newValue) {
                if (newValue !== value) {
                    observer(newValue);
                    value = newValue;
                    update();
                }
            }
        })
    }
    observer(obj);
    // AOP
    // obj.name = {n:200}; // 数据变了 需要更新视图 深度监控
    // obj.name.n = 100;
    obj.push(123);
    obj.push(456);
    console.log(obj);

首先定义了一个 update 函数,用于在数据变化时更新视图。接着创建了一个数组对象 obj,并定义了一个 observer 函数,该函数判断传入的值是不是一个对象,如果是对象,就遍历对象的所有属性,给每个属性添加 getter 和 setter,从而实现数据劫持。如果是一个数组,就对数组的原型对象进行克隆,并重写了数组对象的 pushshift 方法,以便在数据变化时能够自动更新视图。

defineReactive 函数用于定义一个对象属性的 getter 和 setter,其中 get 方法返回该属性的值,set 方法在该属性被赋新值时更新该属性的值并调用 update 函数更新视图。在 set 方法中,如果新的值与旧的值不同,则先调用 observer 函数,如果新的值也是一个对象,那么会给它的属性添加 getter 和 setter,实现递归的数据劫持。

最后调用 observer 函数,对 obj 进行数据劫持,并给 obj 调用 push 方法添加 AOP,以便在 push 方法被调用时自动更新视图。

最终输出了 obj 的值,即 [1,2,3,123,456]

5 实现一下hash路由


  
  
  
  

简单实现:


封装成一个class:

 const box = document.getElementsByClassName('box')[0];
        
        class HashRouter {
            constructor(hashStr, cb) {
                this.hashStr = hashStr
                this.cb = cb
                this.watchHash()
                this.watch = this.watchHash.bind(this)
                window.addEventListener('hashchange', this.watch)
            }
            watchHash() {
                let hash = window.location.hash.slice(1)
                this.hashStr = hash
                this.cb(hash)
            }
        }
        new HashRouter('red', (color) => {
            box.style.background = color
        })

6 实现redux中间件

 function createStore(reducer) {
            let currentState
            let listeners = []

            function getState() {
                return currentState
            }

            function dispatch(action) {
                currentState = reducer(currentState, action)
                listeners.map(listener => {
                    listener()
                })
                return action
            }

            function subscribe(cb) {
                listeners.push(cb)
                return () => {}
            }

            dispatch({
                type: 'ZZZZZZZZZZ'
            })

            return {
                getState,
                dispatch,
                subscribe
            }
        }

        // 应用实例如下:
        function reducer(state = 0, action) {
            switch (action.type) {
                case 'ADD':
                    return state + 1
                case 'MINUS':
                    return state - 1
                default:
                    return state
            }
        }

        const store = createStore(reducer)

        console.log(store);
        store.subscribe(() => {
            console.log('change');
        })
        console.log(store.getState());
        console.log(store.dispatch({
            type: 'ADD'
        }));
        console.log(store.getState());

 

你可能感兴趣的:(javascript,html,前端)