将虚拟 Dom 转化为真实 Dom
题目描述:JSON 格式的虚拟 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 _render(vnode) {
// 如果是数字类型转化为字符串
if (typeof vnode === "number") {
vnode = String(vnode);
}
// 字符串类型直接就是文本节点
if (typeof vnode === "string") {
return document.createTextNode(vnode);
}
// 普通DOM
const dom = document.createElement(vnode.tag);
if (vnode.attrs) {
// 遍历属性
Object.keys(vnode.attrs).forEach((key) => {
const value = vnode.attrs[key];
dom.setAttribute(key, value);
});
}
// 子数组进行递归操作
vnode.children.forEach((child) => dom.appendChild(_render(child)));
return dom;
}
说说Vue2.0和Vue3.0有什么区别
- 重构响应式系统,使用
Proxy
替换Object.defineProperty
,使用Proxy
优势:- 可直接监听数组类型的数据变化
- 监听的目标为对象本身,不需要像
Object.defineProperty
一样遍历每个属性,有一定的性能提升 - 可拦截
apply、ownKeys、has
等13种方法,而Object.defineProperty
不行 - 直接实现对象属性的新增/删除
- 新增
Composition API
,更好的逻辑复用和代码组织 - 重构
Virtual DOM
- 模板编译时的优化,将一些静态节点编译成常量
slot
优化,将slot
编译为lazy
函数,将slot
的渲染的决定权交给子组件- 模板中内联事件的提取并重用(原本每次渲染都重新生成内联函数)
- 代码结构调整,更便于Tree shaking,使得体积更小
- 使用Typescript替换Flow
9种前端常见的设计模式
1. 外观模式
外观模式是最常见的设计模式之一,它为子系统中的一组接口提供一个统一的高层接口,使子系统更容易使用。简而言之外观设计模式就是把多个子系统中复杂逻辑进行抽象,从而提供一个更统一、更简洁、更易用的API。很多我们常用的框架和库基本都遵循了外观设计模式,比如JQuery就把复杂的原生DOM操作进行了抽象和封装,并消除了浏览器之间的兼容问题,从而提供了一个更高级更易用的版本。其实在平时工作中我们也会经常用到外观模式进行开发,只是我们不自知而已
兼容浏览器事件绑定
let addMyEvent = function (el, ev, fn) {
if (el.addEventListener) {
el.addEventListener(ev, fn, false)
} else if (el.attachEvent) {
el.attachEvent('on' + ev, fn)
} else {
el['on' + ev] = fn
}
};
封装接口
let myEvent = {
// ...
stop: e => {
e.stopPropagation();
e.preventDefault();
}
};
场景
- 设计初期,应该要有意识地将不同的两个层分离,比如经典的三层结构,在数据访问层和业务逻辑层、业务逻辑层和表示层之间建立外观Facade
- 在开发阶段,子系统往往因为不断的重构演化而变得越来越复杂,增加外观Facade可以提供一个简单的接口,减少他们之间的依赖。
- 在维护一个遗留的大型系统时,可能这个系统已经很难维护了,这时候使用外观Facade也是非常合适的,为系系统开发一个外观Facade类,为设计粗糙和高度复杂的遗留代码提供比较清晰的接口,让新系统和Facade对象交互,Facade与遗留代码交互所有的复杂工作。
优点
- 减少系统相互依赖。
- 提高灵活性。
- 提高了安全性
缺点
不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。
2. 代理模式
是为一个对象提供一个代用品或占位符,以便控制对它的访问
假设当A 在心情好的时候收到花,小明表白成功的几率有60%,而当A 在心情差的时候收到花,小明表白的成功率无限趋近于0。小明跟A 刚刚认识两天,还无法辨别A 什么时候心情好。如果不合时宜地把花送给A,花被直接扔掉的可能性很大,这束花可是小明吃了7 天泡面换来的。但是A 的朋友B 却很了解A,所以小明只管把花交给B,B 会监听A 的心情变化,然后选择A 心情好的时候把花转交给A,代码如下:
let Flower = function() {
}
let xiaoming = {
sendFlower: function(target) {
let flower = new Flower()
target.receiveFlower(flower)
}
}
let B = {
receiveFlower: function(flower) {
A.listenGoodMood(function() {
A.receiveFlower(flower)
})
}
}
let A = {
receiveFlower: function(flower) {
console.log('收到花'+ flower)
},
listenGoodMood: function(fn) {
setTimeout(function() {
fn()
}, 1000)
}
}
xiaoming.sendFlower(B)
场景
HTML元 素事件代理
<ul id="ul">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<script>
let ul = document.querySelector('#ul');
ul.addEventListener('click', event => {
console.log(event.target);
});
</script>
优点
- 代理模式能将代理对象与被调用对象分离,降低了系统的耦合度。代理模式在客户端和目标对象之间起到一个中介作用,这样可以起到保护目标对象的作用
- 代理对象可以扩展目标对象的功能;通过修改代理对象就可以了,符合开闭原则;
缺点
处理请求速度可能有差别,非直接访问存在开销
3. 工厂模式
工厂模式定义一个用于创建对象的接口,这个接口由子类决定实例化哪一个类。该模式使一个类的实例化延迟到了子类。而子类可以重写接口方法以便创建的时候指定自己的对象类型。
class Product {
constructor(name) {
this.name = name
}
init() {
console.log('init')
}
fun() {
console.log('fun')
}
}
class Factory {
create(name) {
return new Product(name)
}
}
// use
let factory = new Factory()
let p = factory.create('p1')
p.init()
p.fun()
场景
- 如果你不想让某个子系统与较大的那个对象之间形成强耦合,而是想运行时从许多子系统中进行挑选的话,那么工厂模式是一个理想的选择
- 将new操作简单封装,遇到new的时候就应该考虑是否用工厂模式;
- 需要依赖具体环境创建不同实例,这些实例都有相同的行为,这时候我们可以使用工厂模式,简化实现的过程,同时也可以减少每种对象所需的代码量,有利于消除对象间的耦合,提供更大的灵活性
优点
- 创建对象的过程可能很复杂,但我们只需要关心创建结果。
- 构造函数和创建者分离, 符合“开闭原则”
- 一个调用者想创建一个对象,只要知道其名称就可以了。
- 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。
缺点
- 添加新产品时,需要编写新的具体产品类,一定程度上增加了系统的复杂度
- 考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度
什么时候不用
当被应用到错误的问题类型上时,这一模式会给应用程序引入大量不必要的复杂性.除非为创建对象提供一个接口是我们编写的库或者框架的一个设计上目标,否则我会建议使用明确的构造器,以避免不必要的开销。
由于对象的创建过程被高效的抽象在一个接口后面的事实,这也会给依赖于这个过程可能会有多复杂的单元测试带来问题。
4. 单例模式
顾名思义,单例模式中Class的实例个数最多为1。当需要一个对象去贯穿整个系统执行某些任务时,单例模式就派上了用场。而除此之外的场景尽量避免单例模式的使用,因为单例模式会引入全局状态,而一个健康的系统应该避免引入过多的全局状态。
实现单例模式需要解决以下几个问题:
- 如何确定Class只有一个实例?
- 如何简便的访问Class的唯一实例?
- Class如何控制实例化的过程?
- 如何将Class的实例个数限制为1?
我们一般通过实现以下两点来解决上述问题:
- 隐藏Class的构造函数,避免多次实例化
- 通过暴露一个
getInstance()
方法来创建/获取唯一实例
Javascript中单例模式可以通过以下方式实现:
// 单例构造器
const FooServiceSingleton = (function () {
// 隐藏的Class的构造函数
function FooService() {
}
// 未初始化的单例对象
let fooService;
return {
// 创建/获取单例对象的函数
getInstance: function () {
if (!fooService) {
fooService = new FooService();
}
return fooService;
}
}
})();
实现的关键点有:
- 使用 IIFE创建局部作用域并即时执行;
- getInstance() 为一个 闭包 ,使用闭包保存局部作用域中的单例对象并返回。
我们可以验证下单例对象是否创建成功:
const fooService1 = FooServiceSingleton.getInstance();
const fooService2 = FooServiceSingleton.getInstance();
console.log(fooService1 === fooService2); // true
场景例子
- 定义命名空间和实现分支型方法
- 登录框
- vuex 和 redux中的store
优点
- 划分命名空间,减少全局变量
- 增强模块性,把自己的代码组织在一个全局变量名下,放在单一位置,便于维护
- 且只会实例化一次。简化了代码的调试和维护
缺点
- 由于单例模式提供的是一种单点访问,所以它有可能导致模块间的强耦合
- 从而不利于单元测试。无法单独测试一个调用了来自单例的方法的类,而只能把它与那个单例作为一 个单元一起测试。
5. 策略模式
策略模式简单描述就是:对象有某个行为,但是在不同的场景中,该行为有不同的实现算法。把它们一个个封装起来,并且使它们可以互相替换
<html>
<head>
<title>策略模式-校验表单</title>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
</head>
<body>
<form id = "registerForm" method="post" action="http://xxxx.com/api/register">
用户名:<input type="text" name="userName">
密码:<input type="text" name="password">
手机号码:<input type="text" name="phoneNumber">
<button type="submit">提交</button>
</form>
<script type="text/javascript">
// 策略对象
const strategies = {
isNoEmpty: function (value, errorMsg) {
if (value === '') {
return errorMsg;
}
},
isNoSpace: function (value<