> 文章列表 > 框架相关手写题

框架相关手写题

框架相关手写题

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<div id="app"><span><a></a></span><span><a></a><a></a></span>
</div>
  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对象就可以了
<div id="app"><h3>姓名</h3><p>{{name}}</p><h3>年龄</h3><p>{{age}}</p></div>
 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 和 setterObject.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 重置为 nullDep.target = null;} // 如果该节点有子节点,则递归调用 compile 函数处理子节点else if (child.firstElementChild) {this.compile(child);}})}}class Dep {constructor() {this.subNode = [];}// 添加一个新的节点到subNode数组中addSubNode(node) {this.subNode.push(node);}// 遍历subNode数组,更新节点的内容为newValupdate(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.definePropertylet 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 reflectif (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和setterObject.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路由

<html><style>html, body {margin: 0;height: 100%;}ul {list-style: none;margin: 0;padding: 0;display: flex;justify-content: center;}.box {width: 100%;height: 100%;background-color: red;}</style><body><ul><li><a href="#red">红色</a></li><li><a href="#green">绿色</a></li><li><a href="#purple">紫色</a></li></ul></body>
</html>

简单实现:

<script>const box = document.getElementsByClassName('box')[0];const hash = location.hashwindow.onhashchange = function (e) {const color = hash.slice(1)box.style.background = color}
</script>

封装成一个class:

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

6 实现redux中间件

 function createStore(reducer) {let currentStatelet 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 + 1case 'MINUS':return state - 1default: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());

 

拔智齿网