vue 生成文件格式说明
vue 是通过 rollup 进行打包的,rollup 可以通过配置 output 的 format 值打包成不同格式的文件
// rollup.config.js
export default {
input: 'package/vue/index.ts',
output: {
file: 'dist/index.js',
format: 'amd',//amd cjs esm iife umd
name: 'Vue',
}
}
// package/vue/index.ts
const Vue={
createApp(){}
};
export default Vue;
amd : 异步模块定义,用于像RequireJS这样的模块加载器
cjs : CommonJS,适用于 Node 和 Browserify/Webpack
esm : 将软件包保存为 ES 模块文件,在现代浏览器中可以通过 <script type=module> 标签引入
iife : 一个立即执行函数的功能,适合作为<script>标签
umd – 通用模块定义,以amd,cjs 和 iife 为一体
// dist/index.js
// cjs 格式
'use strict';
const Vue={
createApp(){}
};
module.exports = Vue;
// esm 格式
const Vue={
createApp(){}
};
export { Vue as default };
// amd 格式
define((function () { 'use strict';
const Vue={
createApp(){}
};
return Vue;
}));
// iife 格式
var Vue = (function () {
'use strict';
const Vue={
createApp(){}
};
return Vue;
})();
// umd 格式
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Vue = factory());
})(this, (function () { 'use strict';
const Vue={
createApp(){}
};
return Vue;
}));
vue 的设计思路
声明式框架
对于视图(UI)框架来说,从范式的角度看,通常分为命令式框架和声明式框架。命令式框架关注的是过程,声明式框架关注的是结果。
例如,要实现如下如下功能:
- 获取 id 为 #app 的 div 标签
- 设置文本内容是 'hello world'
- 绑定点击事件,点击打印 'OK'
用声明式的方式翻译成代码
const div = document.querySelector('#app');
div.innerText = 'hello world';
div.addEventListener('click',()=>{console.log('ok')})
声明式方式只在乎结果,只需要通过如下一行代码就完成上述功能,但是功能怎么实现的,这个是框架需要考虑的事情。
<div @click="console.log('ok')" >hello world</div>
上面的代码就是 vue 的模板,vue 就是典型的声明式框架,除了 vue 外,react angular 都是声明式的框架。
编译器
在说明声明式框架时提到过 vue 的模板 <div @click="console.log('ok')" >hello world</div> 。可以看出它和 HTML 的书写方式基本一致,但仅是看上去不一样。对于 vue 的编译器来说,模板就是一个普通的字符串。
编译器的功能就是把模板里的字符串转变为渲染函数,渲染函数执行后会得到一个JavaScript 对象,该对象也就是虚拟DOM。
下面用一个 .vue 文件简单说明下编译器的工作原理
<template>
<div @click="handler">
<span>{{name}}</span>
</div>
</template>
<script>
export default{
data(){
return {
name:'a'
}
},
methods:{
handler(){}
}
}
</script>
通过编译器的编译,模板将会编译成渲染函数并添加到 <script> 标签快的组件对象上,最终在浏览器运行的代码如下:
export default{
data(){
return {
name:'a'
}
},
methods:{
handler(){}
},
render(h){
return h('div',{onClick:handler},[h('span',this.name)])//返回的就是虚拟 DOM
}
}
将 h 函数的结果打印出来可以得到虚拟DOM,在Vue2中对象包含的属性如下:
VNode {tag: 'div', data: {…}, children: Array(1), text: undefined, elm: undefined, …}
// children: Array(1) 的结果如下
VNode {tag: 'span', data: undefined, children: Array(1), text: undefined, elm: span, …}
顺便说一点,通过 .vue 文件打包后得到的代码就是 render 函数,因此打包后的文件是不带渲染器的。
渲染器
当了解了虚拟 DOM 其实就是描述真实 DOM 的对象后,那 vue 又是怎么把虚拟 DOM 变成真实 DOM 渲染到页面上的呢?答案是渲染器,那么渲染器是如何工作的,下面通过一个简单的例子来说明。
假设有如下虚拟 DOM:
const VNode = {
tag: 'div',
props: {
onClick: () => {
console.log('ok')
}
},
text: undefined,
children: [{
tag: 'span',
text: undefined,
children: [{
tag: undefined,
text: 1,
children: undefined,
}]
}]
};
编写一个渲染器函数 renderer
function renderer(VNode, container) {
let el;
// 创建元素
VNode.tag && (el = document.createElement(VNode.tag));
// 为元素条件属性和事件
VNode.props && Object.keys(VNode.props).forEach(keys => {
if (/^on/s.test(keys)) {
const eventType = keys.slice(2).toLowerCase();
el.addEventListener(eventType, () => {
VNode.props[keys].call(VNode);
})
}
});
// 处理 children
if (Array.isArray(VNode.children)) {
VNode.children.forEach(child => {
renderer(child, el)
})
}
// 元素/文本挂载
if (VNode.text) {
container.innerText = VNode.text;
} else {
container.appendChild(el)
}
}
renderer(VNode,document.body);
在浏览器中运行这段代码,点击 div 标签会打印出 ok。可以看出渲染器其实就是操作 DOM API来完成渲染工作。当然 vue 的渲染器不仅仅是创建 DOM 节点这么简单,其精髓是在节点更新阶段的 patch 算法。
组件的本质
初步了解了编译器,虚拟 DOM 和渲染器后,我们知道了 DOM 元素会先经编译器转化为 render 函数,render 函数执行得到虚拟 DOM,虚拟 DOM 经渲染器创建成真实的 DOM。
在 vue 的使用过程中,最常用的是组件,组件在模板中也是通过标签的形式展示的。但在本质上 组件其实就是一组 DOM 元素的封装。
const myComponent1 =function(){
return {
tag: 'div',
props: {
onClick: () => {
console.log('ok')
}
},
text: undefined,
children: [{
tag: 'span',
text: undefined,
children: [{
tag: undefined,
text: 1,
children: undefined,
}]
}]
}
}
const VNode={
tag:myComponent1
}
上面的例子中,组件是通过一个函数来表示的,函数执行后得到的是虚拟DOM,虚拟DOM描述的就是一组 DOM 元素。当然除了函数外通过对象也是可以用来描述组件的。下面看下如何用对象描述组件:
const myComponent = {
render(){
return {
tag: 'div',
props: {
onClick: () => {
console.log('ok')
}
},
text: undefined,
children: [{
tag: 'span',
text: undefined,
children: [{
tag: undefined,
text: 1,
children: undefined,
}]
}]
}
}
}
const VNode={
tag:myComponent2
}
为了兼容组件的渲染,渲染器 renderer 函数需要做如下修改:
function renderer(VNode, container) {
// 判断是组件还是普通元素
if (typeof VNode.tag === 'function') {
mountComponent(VNode,container);
} else if (typeof VNode.tag === "object") {
mountComponent(VNode,container);
}else if(typeof VNode.tag === 'string'){
mountElement(VNode, container)
}
}
function mountComponent(VNode,container){
if(typeof VNode.tag === 'function'){
const VNodes = VNode.tag();
mountElement(VNodes, container);
}else{
const VNodes = VNode.tag.render();
mountElement(VNodes, container);
}
}
function mountElement(VNode, container) {
let el;
// 创建元素
VNode.tag && (el = document.createElement(VNode.tag));
// 为元素条件属性和事件
VNode.props && Object.keys(VNode.props).forEach(keys => {
if (/^on/s.test(keys)) {
const eventType = keys.slice(2).toLowerCase();
el.addEventListener(eventType, () => {
VNode.props[keys].call(VNode);
})
}
});
// 处理 children
if (Array.isArray(VNode.children)) {
VNode.children.forEach(child => {
mountElement(child, el)
})
}
// 元素/文本挂载
if (VNode.text) {
container.innerText = VNode.text;
} else {
container.appendChild(el)
}
}
renderer(VNode, document.querySelector('#app1'));
renderer(VNode1, document.querySelector('#app2'));
renderer(VNode2, document.querySelector('#app3'));
响应式系统
响应式系统可以说是 vue 的驱动器,初始化时读取模板中的数据触发 getter,此时收集渲染函数;模板中的数据被修改触发 setter,此时执行收集到的渲染函数。关于响应式系统接下来会通过手写一个响应式系统加深理解。