vue.js 设计与实现 - 框架
2023-04-17
前端Vue
框架范式
视图框架通常有命令式、声明式,各有优劣。
命令式重过程,即一步步做:
js
const div = document.querySelector('#div') // 获取 div
div.textContent = 'hello world' // 赋文本
div.addEventListener('click', () => alert('ok')) // 绑事件
声明式重结果,过程由框架处理:
vue
<div @click="alert('ok')">
hello world
</div>
性能
声明式框架之所以简约,乃封装命令式代码于其内。故其性能不优于命令式。因而开发声明式框架,其核心关注点,在于性能优化。
虚拟 DOM
声明式更新能耗 = 找差异能耗 + 修改能耗
其中,修改能耗及命令式代码。因而降低找差异能耗,声明式性能即可无限接近命令式。虚拟 DOM 应运而生。
虚拟 DOM 创建页面分两步:
- 创建 js 对象,用以描述 DOM
- 递归遍历虚拟 DOM 树,创建真 DOM
渲染器
渲染器用以将虚拟 DOM 转换为真实 DOM
js
const vnode = {
tag: 'div',
props: {
onClick: () => alert('hello')
},
children: 'click me'
}
/** 渲染器 */
function render(vnode, container) {
// 创建元素
const el = document.createElement(vnode.tag)
// 处理属性
for (const key in vnode.props) {
// 以 on 打头,均为事件,注册之
if (key.startsWith('on')) {
el.addEventListener(key.slice(2).toLowerCase(), vnode.props[key])
}
}
// 处理子元素
// 子元素为字符串,则追加文本节点
if (typeof vnode.children === 'string') {
el.appendChild(document.createTextNode(vnode.children))
}
// 为数组,则递归
else if (Array.isArray(vnode.children)) {
// 递归
vnode.children.forEach(child => render(child, el))
}
// 挂载
container.appendChild(el)
}
// 调用
render(vnode, document.querySelector('#app'))
由上述代码可知,渲染器原理极为简单。
组件
组件就是一组 DOM 元素,封装之以复用。函数也是一组代码的封装。因而函数可用来表示组件,其返回值为欲渲染内容:
js
const MyComponent = function () {
return {
tag: 'div',
props: {
onClick: () => alert('hello component')
},
children: '点我'
}
}
tag: 'div'
表示 <div>
,同理, tag: MyComponent
表示组件。
修改渲染函数:
js
function renderer(vnode, container) {
// 字符串,表明该 vnode 为普通标签
if (typeof vnode.tag === 'string') {
mountElement(vnode, container)
}
// 函数,表明是组件
else if (typeof vnode.tag === 'function') {
mountComponent(vnode, container)
}
}
/** 挂载普通元素 */
function mountElement(vnode, container) {
// 创建元素
const el = document.createElement(vnode.tag)
// 遍历属性
for (const key in vnode.props) {
if (key.startsWith('on')) {
// 事件
el.addEventListener(key.slice(2).toLowerCase(), vnode.props[key])
}
}
// 处理子元素
if (typeof vnode.children === 'string') {
el.appendChild(document.createTextNode(vnode.children))
}
else if (Array.isArray(vnode.children)) {
vnode.children.forEach(child => renderer(child, el))
}
// 挂载
container.appendChild(el)
}
/** 挂载组件 */
function mountComponent(vnode, container) {
// 调用组件函数,获取其内容,即虚拟 DOM
const subtree = vnode.tag()
// 递归调用 renderer
renderer(subtree, container)
}
编译器
编译器用以模板转渲染函数,如:
vue
<div @click="handle">
点击
</div>
转化为:
js
function render() {
return {
tag: 'div',
props: {
onClick: handler
},
children: '点击'
}
}