写作计划
欢迎大家持续关注、首先做一个简单的计划。
这个计划一定会变😜,要不然怎么叫迭代呢。
📎 Mock状态
🚀简版实现
Step | 00 | 01 | 02 | 03 | 04 | 05 | 06 | |
响应式逻辑 | - | 📎 | 🚀 | 🚀 | 🚀 | 🚀 | 🚀 | |
编译函数 | - | 📎 | 📎 | 📎 | 🚀 | 🚀 | 🚀 | |
Parser | - | 📎 | 📎 | 🚀 | 🚀 | 🚀 | ||
Transformer | - | - | 📎 | 📎 | 🚀 | 🚀 | 🚀 | |
Generator | - | - | 📎 | 📎 | 🚀 | 🚀 | 🚀 | |
运行时环境 | - | 📎 | 📎 | 🚀 | 🚀 | 🚀 | 🚀 | |
渲染器 | - | 📎 | 📎 | 📎 | 🚀 | 🚀 | 🚀 | |
核心特性 | Dom Diff | - | - | - | - | - | 🚀 | 🚀 |
静态提升 | - | - | - | - | - | - | 🚀 | |
自定义渲染器 | - | - | - | - | - | - | 🚀 | |
- | - | - | - | - | - | 🚀 |
Step00 NoMVVM
想象一下如果没有MVVM框架我们要怎么实现一个这样的功能。
- 创建一个数据模型
const data = { message: 'Hello Vue 3!!' }
- 创建一个视图
<div id='app'> <input /> <button></button> </div>
- 创建一个将模型数据更新到视图上的渲染函数
function update() { // 更新视图 document.querySelector('button').innerHTML = data.message document.querySelector('input').value = data.message }
- 执行首次数据更新
// 首次数据渲染 update()
- 绑定按钮点击事件
- 修改模型中数据: 反转字符串
- 修改模型后重新渲染数据
document.querySelector('button').addEventListener('click', function () { data.message = data.message.split('').reverse().join('') update() })
- 对输入项变化进行监听
- 数据项变化时修改模型中数据
- 修改模型后重新渲染数据
document.querySelector('input').addEventListener('keyup', function () { data.message = this.value update() })
Step01 总体架构 - MVVM(Mock版)
MVVM原理
MVVM框架其实就是在原先的View和Model之间增加了一个VM层完成以下工作。完成数据与视图的监听。我们这一步先写一个Mock版本。其实就是先针对固定的视图和数据模型实现监听。
接口定义
我们MVVM的框架接口和Vue3一模一样。
初始化需要确定
- 视图模板
- 数据模型
- 模型行为 - 比如我们希望click的时候数据模型的message会会倒序排列。
const App = { // 视图模板 template: ` <input v-model="message"/> <button @click='click'>{{message}}</button> `, // 数据模型 data() { return { message: 'Hello Vue 3!!' } }, // 行为函数 methods: { click() { this.message = this.message.split('').reverse().join('') } } } const { createApp } = Vue createApp(App).mount('#app')
程序骨架
const Vue = { createApp(config) { // 编译过程 const compile = (template) => (observed, dom) => { } // 生成渲染函数 const render = compile() // 定义响应函数 let effective // 数据劫持 observed = new Proxy(config.data(), { }) return { // 初始化 mount: function (container) { } } } }
编译渲染函数
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中的值,就会打印出来。
也就是说如果对象被修改就会得的被响应。
image-20200713122621925
当然我们需要的响应就是重新更新视图也就是重新运行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)
视图变化的监听
浏览器视图的变化,主要体现在对输入项变化的监听上,所以只需要通过绑定监听事件就可以了。
document.querySelector('input').addEventListener('keyup', function () { data.message = this.value })
完整的代码
<html lang="en"> <body> <div id='app'></div> <script> const App = { // 视图 template: ` <input v-model="message"/> <button @click='click'>{{message}}</button> `, data() { return { message: 'Hello Vue 3!!' } }, methods: { click() { this.message = this.message.split('').reverse().join('') } } } const Vue = { createApp(config) { // 编译过程 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 } // 生成渲染函数 const render = compile() // 定义响应函数 let effective // 数据劫持 observed = new Proxy(config.data(), { set(target, key, value, receiver) { const ret = Reflect.set(target, key, value, receiver) // 触发函数响应 effective() return ret }, }) return { mount: function (container) { const dom = document.querySelector(container) effective = () => render(observed, dom) render(observed, dom) } } } } const { createApp } = Vue createApp(App).mount('#app') </script> </body> </html>
OK今天写到这,终于完成了第一步虽然大部分还都是固定的至少把大体结构搞定了。
Step02 编译流程(Mock)
这个章节我们主要看看compile这个功能。
上文已经说过编译函数的功能
// 编译函数 // 输入值为视图模板 const compile = (template) => { //渲染函数 return (observed, dom) => { // 渲染过程 } }
简单的说就是
- 输入:视图模板
- 输出:渲染函数
细分起来还可以分为三个个小步骤
Snip20200713_17
- Parse 模板字符串 -> AST(Abstract Syntax Treee)抽象语法树
- Transform 转换标记 譬如 v-bind v-if v-for的转换
- Generate AST -> 渲染函数
// 模板字符串 -> AST(Abstract Syntax Treee)抽象语法树 let ast = parse(template) // 转换处理 譬如 v-bind v-if v-for的转换 ast = transfer(ast) // AST -> 渲染函数 return generator(ast)
- 我们可以通过在线版的VueTemplateExplorer感受一下
https://vue-next-template-explorer.netlify.com/
image-20200713150630150
“
Parse解析器
解析器的工作原理其实就是一连串的正则匹配。
比如:
标签属性的匹配
- class="title"
- class='title'
- class=title
const attr = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)=("([^"]*)"|'([^']*)'|([^\s"'=<>`]+)/ "class=abc".match(attr); // output (6) ["class=abc", "class", "abc", undefined, undefined, "abc", index: 0, input: "class=abc", groups: undefined] "class='abc'".match(attr); // output (6) ["class='abc'", "class", "'abc'", undefined, "abc", undefined, index: 0, input: "class='abc'", groups: undefined]
这个等实现的时候再仔细讲。可以参考一下文章。
那对于我们的项目来讲就可以写成这个样子
// <input v-model="message"/> // <button @click='click'>{{message}}</button> // 转换后的AST语法树 const parse = template => ({ children: [{ tag: 'input', props: { name: 'v-model', exp: { content: 'message' }, }, }, { tag: 'button', props: { name: '@click', exp: { content: 'message' }, }, content:'{{message}}' } ], })
Transform转换处理
前一段知识做的是抽象语法树,对于Vue3模板的特别转换就是在这里进行。
比如:vFor、vOn
在Vue三种也会细致的分为两个层级进行处理
- compile-core 核心编译逻辑
- AST-Parser
- 基础类型解析 v-for 、v-on
image-20200713183256931
- compile-dom 针对浏览器的编译逻辑
- v-html
- v-model
- v-clock
image-20200713183210079
const transfer = ast => ({ children: [{ tag: 'input', props: { name: 'model', exp: { content: 'message' }, }, }, { tag: 'button', props: { name: 'click', exp: { content: 'message' }, }, children: [{ content: { content: 'message' }, }] } ], })
Generate生成渲染器
生成器其实就是根据转换后的AST语法树生成渲染函数。当然针对相同的语法树你可以渲染成不同结果。比如button你希望渲染成 button还是一个svg的方块就看你的喜欢了。这个就叫做自定义渲染器。这里我们先简单写一个固定的Dom的渲染器占位。到后面实现的时候我在展开处理。
const generator = ast => (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 }
关于发布时间
具体时间可以看大家可以看看官方时间表。官方时间表
目前在Vue3处于Beta版本,后面主要是处理稳定性问题。也就是说主要Api不会有很多改进。尤大神从直播中说虽然很多想法,但是大的变化最快也会出现在3.1上面了。所以目前的版本应该应该和正式版差异很小了。 看来Q2能发布的可能性极大。