Appearance
实现一个简单插值表达式(模板引擎)
熟悉 Vue 的小伙伴都知道我们在 template 中使用 state 状态可以使用插值表达式
现在我们也来实现一个简单的插值表达式,希望能够达到如下的效果:
已知有个数据结构:
js
const site = {
names: 'jimmy 知识星球',
article: 50,
comment: 50,
}
const site = {
names: 'jimmy 知识星球',
article: 50,
comment: 50,
}
表达式 | 结果 |
---|---|
site.names | jimmy 知识星球 |
site.article+site.comment | 100 |
原理解析
主要可分为以下几步:
先解析模板语法,将可能是插值表达式的文本(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)
页面效果
总结
只是完成简单的模板插值表达式的编译还是比较简单的,欢迎小伙伴们关注和 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)
}
}
}
常见的案例:
监听滑动,第一时间展示小图标
当滚动至顶部,出现特殊区域,不在顶部时,自动关闭显示特殊区域
埋点需求
监听用户一段时间内的一些操作
....
其他
以上的实现代码都使用 闭包 的特性,因为防抖和节流函数自身都是有状态的,在上面的例子分别是timer
、timeout
,使用闭包的特性是最好的,不会被外界环境所污染。
实现 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 二者值一直保持同步
*/
总结下来实现的步骤其实就是两步:
收集依赖关系(副作用收集)
使用
effect
函数来接受一个个的副作用函数当数据变化时执行副作用函数
依赖收集
使用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
,来实现,二者都是比基础的array
和object
有着更高的性能,也能将我们所学的知识都融合串起来。有看过Vue3
源码的小伙伴就会知道,Vue3
这块用了性能更优的 API,就是WeakSet
和WeakMap
,小伙伴们也可以用起来,这里我就使用Map
和Set
了。
完整代码
项目地址:传送门
如果对你有帮助的话~欢迎点个 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)
当我们开发相对大一点的项目时,一般都会上状态管理,像vuex
、redux
之类的全局状态。它们都有共同的点,就是数据都是 单向流,下面我们实现一个简单版本的redux
,复习一下redux
的API(因为真的很经典,面试也很容易被问到);
状态管理的特点:
数据存储的是公用数据,且是单向的
获取状态只能通过
getState()
数据都是只读数据
修改数据只能通过
dispatch()
当全局状态发生改变时我们可以捕获到这个动作,做出一些操作:
绑定修改监听器 通过
effect()
redux图示:
我们知道了最重要的是我们要实现三个方法: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
版本的状态管理。