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