一、整体工作流程
- 编译器将视图模板编译为渲染函数
- 数据响应模块将数据对象初始化为响应式数据对象
- 视图渲染
- RenderPhase : 渲染模块使用渲染函数根据初始化数据生成虚拟Dom
b. MountPhase : 利用虚拟Dom创建视图页面Html
c.PatchPhase:数据模型一旦变化渲染函数将再次被调用生成新的虚拟Dom,然后做Dom Diff更新视图Html
二、三大模块的分工
- 数据响应式模块
- 编译器
- 渲染函数
1. 数据响应式模块
提供创建一切数据变化都是可以被监听的响应式对象的方法。
2. 编译模块
将html模板编译为渲染函数
这个编译过程可以在一下两个时刻执行
- 浏览器运行时 (runtime)
- Vue项目打包编译时 (compile time)
3. 渲染函数
渲染函数通过以下三个周期将视图渲染到页面上
- Render Phase
- Mount Phase
- Patch Phase
三、MVVM原型(Mock版)
MVVM框架其实就是在原先的View和Model之间增加了一个VM层完成以下工作。完成数据与视图的监听。我们这一步先写一个Mock版本。其实就是先针对固定的视图和数据模型实现监听。
1. 接口定义
我们MVVM的框架接口和Vue3一模一样。
初始化需要确定
- 视图模板
- 数据模型
- 模型行为 - 比如我们希望click的时候数据模型的message会会倒序排列。
const App = { // 视图 template: ` <input v-model="message"/> <button @click='click'>{{message}}</button> `, setup() { // 数据劫持 const state = new Proxy( { message: "Hello Vue 3!!", }, { set(target, key, value, receiver) { const ret = Reflect.set(target, key, value, receiver); // 触发函数响应 effective(); return ret; }, } ); const click = () => { state.message = state.message.split("").reverse().join(""); }; return { state, click }; }, }; const { createApp } = Vue; createApp(App).mount("#app");
2. 程序骨架
程序执行过程大概如图:
const Vue = { createApp(config) { // 编译过程 const compile = (template) => (content, dom) => { }; // 生成渲染函数 const render = compile(config.template); return { mount: function (container) { const dom = document.querySelector(container); // 实现setup函数 const setupResult = config.setup(); // 数据响应更新视图 effective = () => render(setupResult, dom); render(setupResult, dom); }, }; }, };
3. 编译渲染函数
MVVM框架中的渲染函数是会通过视图模板的编译建立的。
// 编译函数 // 输入值为视图模板 const compile = (template) => { //渲染函数 return (observed, dom) => { // 渲染过程 } }
简单的说就是对视图模板进行解析并生成渲染函数。
大概要处理以下三件事
- 确定哪些值需要根据数据模型渲染
// <button>{{message}}</button> // 将数据渲染到视图 button = document.createElement('button') button.innerText = observed.message dom.appendChild(button)
- 绑定模型事件
// <button @click='click'>{{message}}</button> // 绑定模型事件 button.addEventListener('click', () => { return config.methods.click.apply(observed) })
- 确定哪些输入项需要双向绑定
// <input v-model="message"/> // 创建keyup事件监听输入项修改 input.addEventListener('keyup', function () { observed.message = this.value })
完整的代码
const compile = (template) => (observed, dom) => { // 重新渲染 let input = dom.querySelector('input') if (!input) { input = document.createElement('input') input.setAttribute('value', observed.message) input.addEventListener('keyup', function () { observed.message = this.value }) dom.appendChild(input) } let button = dom.querySelector('button') if (!button) { console.log('create button') button = document.createElement('button') button.addEventListener('click', () => { return config.methods.click.apply(observed) }) dom.appendChild(button) } button.innerText = observed.message }
四、数据响应实现
Vue普遍走的就是数据劫持方式。不同的在于使用DefineProperty还是Proxy。也就是一次一个属性劫持还是一次劫持一个对象。当然后者比前者听着就明显有优势。这也就是Vue3的响应式原理。
Proxy/Reflect是在ES2015规范中加入的,Proxy可以更好的拦截对象行为,Reflect可以更优雅的操纵对象。 优势在于
- 针对整个对象定制 而不是对象的某个属性,所以也就不需要对keys进行遍历。
- 支持数组,这个DefineProperty不具备。这样就省去了重载数组方法这样的Hack过程。
- Proxy 的第二个参数可以有 13 种拦截方法,这比起 Object.defineProperty() 要更加丰富
- Proxy 作为新标准受到浏览器厂商的重点关注和性能优化,相比之下 Object.defineProperty() 是一个已有的老方法
- 可以通过递归方便的进行对象嵌套。
说了这么多我们先来一个小例子
var obj = new Proxy({}, { get: function (target, key, receiver) { console.log(`getting ${key}!`); return Reflect.get(target, key, receiver); }, set: function (target, key, value, receiver) { console.log(`setting ${key}!`); return Reflect.set(target, key, value, receiver); } }) obj.abc = 132
这样写如果你修改obj中的值,就会打印出来。
也就是说如果对象被修改就会得的被响应。
当然我们需要的响应就是重新更新视图也就是重新运行render方法。
首先制造一个抽象的数据响应函数
// 定义响应函数 let effective observed = new Proxy(config.data(), { set(target, key, value, receiver) { const ret = Reflect.set(target, key, value, receiver) // 触发函数响应 effective() return ret }, })
在初始化的时候我们设置响应动作为渲染视图
const dom = document.querySelector(container) // 设置响应动作为渲染视图 effective = () => render(observed, dom) render(observed, dom)
1. 视图变化的监听
浏览器视图的变化,主要体现在对输入项变化的监听上,所以只需要通过绑定监听事件就可以了。
document.querySelector('input').addEventListener('keyup', function () { data.message = this.value })
2. 完整的代码
<html lang="en"> <body> <div id="app"></div> <script> const Vue = { createApp(config) { // 编译过程 const compile = (template) => (content, dom) => { // 重新渲染 dom.innerText = ""; input = document.createElement("input"); input.addEventListener("keyup", function () { content.state.message = this.value; }); input.setAttribute("value", content.state.message); dom.appendChild(input); let button = dom.querySelector("button"); button = document.createElement("button"); button.addEventListener("click", () => { return content.click.apply(content.state); }); button.innerText = content.state.message; dom.appendChild(button); }; // 生成渲染函数 const render = compile(config.template); return { mount: function (container) { const dom = document.querySelector(container); const setupResult = config.setup(); effective = () => render(setupResult, dom); render(setupResult, dom); }, }; }, }; // 定义响应函数 let effective; const App = { // 视图 template: ` <input v-model="message"/> <button @click='click'>{{message}}</button> `, setup() { // 数据劫持 const state = new Proxy( { message: "Hello Vue 3!!", }, { set(target, key, value, receiver) { const ret = Reflect.set(target, key, value, receiver); // 触发函数响应 effective(); return ret; }, } ); const click = () => { state.message = state.message.split("").reverse().join(""); }; return { state, click }; }, }; const { createApp } = Vue; createApp(App).mount("#app"); </script> </body> </html>
五、 视图渲染过程
Dom => virtual DOM => render functions
1. 什么是Dom 、Document Object Model
HTML在浏览器中会映射为一些列节点,方便我们去调用。
2. 什么是虚拟Dom
Dom中节点众多,直接查询和更新Dom性能较差。
A way of representing the actual DOM with JavaScript Objects. 用JS对象重新表示实际的Dom
3. 什么是渲染函数
在Vue中我们通过将视图模板(template)编译为渲染函数(render function)再转化为虚拟Dom
4. 通过DomDiff高效更新视图
5. 总结
举个栗子🌰 虚拟Dom和Dom就像大楼和大楼设计图之间的关系。
假设你要在29层添加一个厨房 ❌ 拆除整个29层,重新建设 ✅先绘制设计图,找出新旧结构不同然后建设