Skip to content

实现一个简单插值表达式(模板引擎)

熟悉 Vue 的小伙伴都知道我们在 template 中使用 state 状态可以使用插值表达式

现在我们也来实现一个简单的插值表达式,希望能够达到如下的效果:

已知有个数据结构:

js
const site = {
	names: 'jimmy 知识星球',
	article: 50,
	comment: 50,
}
const site = {
	names: 'jimmy 知识星球',
	article: 50,
	comment: 50,
}

表达式结果
site.namesjimmy 知识星球
site.article+site.comment100

原理解析

主要可分为以下几步:

  • 先解析模板语法,将可能是插值表达式的文本(string 取出来)

  • 解析取出来的插值表达式,将插值表达式的大括号替换为 js 的模板语法${a}

  • 使用 eval 函数解析以下这个语法,实现内部表达式替换为真实的数据

    这里使用到了eval函数,这个熟悉webpack的小伙伴肯定对这个函数不会陌生的,其会尝试将字符串理解成 js 代码去执行,正是因为这个我们才能将正则解析的结果实现真实转换:site.name => jimmy 知识星球

代码

JS 逻辑代码:

js
const site = {
	name: 'jimmy 知识星球',
	article: 50,
	comment: 50,
}

/**
 *
 * @param {string} string
 */
function generate(string) {
	let template = string.replace(/\{\{([^}]+)\}\}/g, (a, b) => {
		return eval(`${b}`)
	})
	return template
}

console.log(generate('{{ site.article + site.comment }}'))
const site = {
	name: 'jimmy 知识星球',
	article: 50,
	comment: 50,
}

/**
 *
 * @param {string} string
 */
function generate(string) {
	let template = string.replace(/\{\{([^}]+)\}\}/g, (a, b) => {
		return eval(`${b}`)
	})
	return template
}

console.log(generate('{{ site.article + site.comment }}'))

完整代码:

html
<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<meta http-equiv="X-UA-Compatible" content="IE=edge" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title>Document</title>
	</head>
	<body>
		<h1>插值表达式的实现</h1>
		<p>{{site.name}}</p>
		<p>总数据:{{site.article+site.comment}}</p>
		<script src="./index.js"></script>
	</body>
</html>
<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<meta http-equiv="X-UA-Compatible" content="IE=edge" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title>Document</title>
	</head>
	<body>
		<h1>插值表达式的实现</h1>
		<p>{{site.name}}</p>
		<p>总数据:{{site.article+site.comment}}</p>
		<script src="./index.js"></script>
	</body>
</html>
js
/**
 * 模板编译
 *  目标实现和 vue 一样的插值表达式
 */

const site = {
	name: 'jimmy 知识星球',
	article: 50,
	comment: 50,
}

/**
 *
 * @param {string} string
 */
function generate(string) {
	console.log('string', string)
	let template = string.replace(/\{\{([^}]+)\}\}/g, (a, b) => {
		return eval(`${b}`)
	})
	return template
}

const body = document.body
const deepCheck = element => {
	const children = [...element.children]
	console.log(children)
	if (children.length > 0) {
		children.forEach(child => {
			deepCheck(child)
		})
	} else {
		const insetExpression = generate(element.textContent)
		element.textContent = insetExpression
	}
}

deepCheck(body)
/**
 * 模板编译
 *  目标实现和 vue 一样的插值表达式
 */

const site = {
	name: 'jimmy 知识星球',
	article: 50,
	comment: 50,
}

/**
 *
 * @param {string} string
 */
function generate(string) {
	console.log('string', string)
	let template = string.replace(/\{\{([^}]+)\}\}/g, (a, b) => {
		return eval(`${b}`)
	})
	return template
}

const body = document.body
const deepCheck = element => {
	const children = [...element.children]
	console.log(children)
	if (children.length > 0) {
		children.forEach(child => {
			deepCheck(child)
		})
	} else {
		const insetExpression = generate(element.textContent)
		element.textContent = insetExpression
	}
}

deepCheck(body)

页面效果

image-20220518221547078

总结

只是完成简单的模板插值表达式的编译还是比较简单的,欢迎小伙伴们关注和 star

防抖和节流

防抖和节流是两个比较重要的性能优化知识点,在面试的时候也是一个相对比较高频的点,虽然在开发中我们可能会比较喜欢用lodash这种工具库封装好的方法,但是也需要知道应该如何使用,以及他们的基本概念:

防抖

一系列的操作只以最后一次操作为准!

可以使用setTimeout来实现防抖的功能:

js
function debounce(fn, delay) {
	let timer
	return (...args) => {
		if (timer) {
			clearTimeout(timer)
		}

		// 重新调用setTimeout
		timer = setTimeout(() => {
			fn.apply(this, args)
		}, delay)
	}
}
function debounce(fn, delay) {
	let timer
	return (...args) => {
		if (timer) {
			clearTimeout(timer)
		}

		// 重新调用setTimeout
		timer = setTimeout(() => {
			fn.apply(this, args)
		}, delay)
	}
}

常见的案例:

  • 输入框联想搜索商品

    会短时间内监听最后一次输入,将这次输入之后的文本做一些商品匹配

  • ...

节流

一系列操作只以第一次操作为准!(一段时间内最多只会执行一次)

涉及时间,所以判断当前时间和上一次操作的时间间隔是否符合节流时间,如果在时间内则不执行,否则重新执行方法并刷新下次限定的时间:

js
function throttle(fn, delay) {
	let timeout = 0
	return (...args) => {
		const now = +Date.now()
		if (now > timeout + delay) {
			timeout = now
			fn.apply(this, args)
		}
	}
}
function throttle(fn, delay) {
	let timeout = 0
	return (...args) => {
		const now = +Date.now()
		if (now > timeout + delay) {
			timeout = now
			fn.apply(this, args)
		}
	}
}

常见的案例:

  • 监听滑动,第一时间展示小图标

    当滚动至顶部,出现特殊区域,不在顶部时,自动关闭显示特殊区域

  • 埋点需求

    监听用户一段时间内的一些操作

  • ....

其他

以上的实现代码都使用 闭包 的特性,因为防抖和节流函数自身都是有状态的,在上面的例子分别是timertimeout,使用闭包的特性是最好的,不会被外界环境所污染。

实现 reactive 响应式数据

学习 vue 的 reactive 的实现,使用Proxy API来实现一个数据响应式的数据。目标效果如下:

js
const a = {
	website: 'http://www.baidu.com',
	age: 22,
}

const b = a.website

a.website = 'http://www.jimmyxuexue.top'

console.log(a.website) // http://www.jimmyxuexue.top
console.log(b) // http://www.baidu.com

/*
  最终目标: b 也是 http://www.jimmyxuexue.top 二者值一直保持同步
*/
const a = {
	website: 'http://www.baidu.com',
	age: 22,
}

const b = a.website

a.website = 'http://www.jimmyxuexue.top'

console.log(a.website) // http://www.jimmyxuexue.top
console.log(b) // http://www.baidu.com

/*
  最终目标: b 也是 http://www.jimmyxuexue.top 二者值一直保持同步
*/

总结下来实现的步骤其实就是两步:

  1. 收集依赖关系(副作用收集)

    使用effect函数来接受一个个的副作用函数

  2. 当数据变化时执行副作用函数

依赖收集

使用ProxyAPI我们可以非常轻松的实现依赖收集,因为对真正的对象做了一层代理,所以当我们访问对象的某个属性时,我们都是能够捕获到这个动作的,进而就可以知道有哪些值是需要处理的副作用。

这个过程最重要的事情就是将数据和对应依赖的关系维护好。

关系大致长这样:

js
// 以下一段伪代码吧~ 大致能表达意思
const map = {
	name: [() => (b = a.name + 1), () => (c = name + '哈哈')],
	age: [() => (d = a.age + 10), () => (e = a.age - 5)],
}
// 以下一段伪代码吧~ 大致能表达意思
const map = {
	name: [() => (b = a.name + 1), () => (c = name + '哈哈')],
	age: [() => (d = a.age + 10), () => (e = a.age - 5)],
}

触发更新

由于ProxyAPI我们也可以很轻松的捕获到对象的值是否发生了改变,当改变时,我们就去触发对应的关系。

当 name 值发生改变时,我们就会去 name 依赖关系中找出 name 对应的依赖数组,依次执行即可,

js
map['name'].forEach(fn => fn())
map['name'].forEach(fn => fn())

其他

这里依赖数组我们可以使用Set,依赖关系可以使用Map,来实现,二者都是比基础的arrayobject有着更高的性能,也能将我们所学的知识都融合串起来。有看过Vue3源码的小伙伴就会知道,Vue3这块用了性能更优的 API,就是WeakSetWeakMap,小伙伴们也可以用起来,这里我就使用MapSet了。

完整代码

项目地址:传送门

如果对你有帮助的话~欢迎点个 star⭐️~

js
let temp = null
const map = new Map()

function effect(fn) {
	temp = fn
	return fn()
}

function reactive(obj) {
	const proxy = new Proxy(obj, {
		get(target, key) {
			if (map.has(key)) {
				map.get(key).add(temp)
			} else {
				map.set(key, new Set())
			}
			temp = null
			return target[key]
		},
		set(target, key, value) {
			target[key] = value
			map.get(key).forEach(fn => fn())
		},
	})
	return proxy
}

const data = reactive({
	name: 'jimmy',
	age: 22,
})

let a = null
let b = null

effect(() => (a = data.name))
effect(() => (b = data.age + 10))

data.name = 'xuexue'
data.age = 18
console.log(a, 'reactive a')
console.log(b, 'reactive b')
let temp = null
const map = new Map()

function effect(fn) {
	temp = fn
	return fn()
}

function reactive(obj) {
	const proxy = new Proxy(obj, {
		get(target, key) {
			if (map.has(key)) {
				map.get(key).add(temp)
			} else {
				map.set(key, new Set())
			}
			temp = null
			return target[key]
		},
		set(target, key, value) {
			target[key] = value
			map.get(key).forEach(fn => fn())
		},
	})
	return proxy
}

const data = reactive({
	name: 'jimmy',
	age: 22,
})

let a = null
let b = null

effect(() => (a = data.name))
effect(() => (b = data.age + 10))

data.name = 'xuexue'
data.age = 18
console.log(a, 'reactive a')
console.log(b, 'reactive b')

实现统一的状态管理(mini-redux)

当我们开发相对大一点的项目时,一般都会上状态管理,像vuexredux之类的全局状态。它们都有共同的点,就是数据都是 单向流,下面我们实现一个简单版本的redux,复习一下redux的API(因为真的很经典,面试也很容易被问到);

状态管理的特点:

  • 数据存储的是公用数据,且是单向的

    获取状态只能通过 getState()

  • 数据都是只读数据

    修改数据只能通过 dispatch()

  • 当全局状态发生改变时我们可以捕获到这个动作,做出一些操作:

    绑定修改监听器 通过 effect()

redux图示:

image-20220525212437315

我们知道了最重要的是我们要实现三个方法:getState()dispath()effect(),除此之外还有一个reducer()函数。

完整代码

源码请点击:传送门

js
/**
 * 实现一个统一的状态管理 -- 类似于 redux
 *  获取状态只能通过 getState()
 *  修改数据只能通过 dispatch()
 *  绑定修改监听器 通过 effect()
 */

/**
 *
 * @param {Function} reducer
 * @param {any} preloadState
 * @returns
 */
function createStore(reducer, preloadState) {
	let currentReducer = reducer
	let currentState = preloadState
	let effective
	return {
		dispatch(action) {
			currentState = currentReducer(currentState, action)
			// 触发通知
			effective && effective() // 有绑定监听函数再执行(用户有可能并未绑定)
		},
		getState() {
			return currentState
		},
		effect(fn) {
			effective = fn
		},
	}
}

function dzReducer(state, action) {
	if (action.type === 'add') {
		console.log('cccc')
		state.age++
		return state
	}
}

let store = createStore(dzReducer, {
	name: 'jimmy',
	age: 22,
})

store.effect(() => {
	console.log('数据发生改变了')
})

console.log(store.getState())

store.dispatch({ type: 'add' })

console.log(store.getState())
/**
 * 实现一个统一的状态管理 -- 类似于 redux
 *  获取状态只能通过 getState()
 *  修改数据只能通过 dispatch()
 *  绑定修改监听器 通过 effect()
 */

/**
 *
 * @param {Function} reducer
 * @param {any} preloadState
 * @returns
 */
function createStore(reducer, preloadState) {
	let currentReducer = reducer
	let currentState = preloadState
	let effective
	return {
		dispatch(action) {
			currentState = currentReducer(currentState, action)
			// 触发通知
			effective && effective() // 有绑定监听函数再执行(用户有可能并未绑定)
		},
		getState() {
			return currentState
		},
		effect(fn) {
			effective = fn
		},
	}
}

function dzReducer(state, action) {
	if (action.type === 'add') {
		console.log('cccc')
		state.age++
		return state
	}
}

let store = createStore(dzReducer, {
	name: 'jimmy',
	age: 22,
})

store.effect(() => {
	console.log('数据发生改变了')
})

console.log(store.getState())

store.dispatch({ type: 'add' })

console.log(store.getState())

控制台会一次输出:

{ name: 'jimmy', age: 22 }
doAction
数据发生改变了
{ name: 'jimmy', age: 23 }
{ name: 'jimmy', age: 22 }
doAction
数据发生改变了
{ name: 'jimmy', age: 23 }

核心代码就大概只有30多行,但是涉及的知识点还是比较细节的,如果小伙伴们有一些redux 知识会领悟的比较快(比如为什么要使用action,action要有type)其实这些都是一个规范。

之后会再实现vuex api 版本的状态管理。