六、实现渲染函数
在Vue中我们通过将视图模板(template)编译为渲染函数(render function)再转化为虚拟Dom
网络异常,图片无法展示
|
渲染流程通常会分为三各部分:
vue-next-template-explorer.netlify.app/
网络异常,图片无法展示
|
- RenderPhase : 渲染模块使用渲染函数根据初始化数据生成虚拟Dom
- MountPhase : 利用虚拟Dom创建视图页面Html
- PatchPhase:数据模型一旦变化渲染函数将再次被调用生成新的虚拟Dom,然后做Dom Diff更新视图Html
mount: function (container) { const dom = document.querySelector(container); const setupResult = config.setup(); const render = config.render(setupResult); let isMounted = false; let prevSubTree; watchEffect(() => { if (!isMounted) { dom.innerHTML = ""; // mount isMounted = true; const subTree = config.render(setupResult); prevSubTree = subTree; mountElement(subTree, dom); } else { // update const subTree = config.render(setupResult); diff(prevSubTree, subTree); prevSubTree = subTree; } }); },
1.Render Phase
渲染模块使用渲染函数根据初始化数据生成虚拟Dom
render(content) { return h("div", null, [ h("div", null, String(content.state.message)), h( "button", { onClick: content.click, }, "click" ), ]); },
2. Mount Phase
利用虚拟Dom创建视图页面Html
function mountElement(vnode, container) { // 渲染成真实的 dom 节点 const el = (vnode.el = createElement(vnode.type)); // 处理 props if (vnode.props) { for (const key in vnode.props) { const val = vnode.props[key]; patchProp(vnode.el, key, null, val); } } // 要处理 children if (Array.isArray(vnode.children)) { vnode.children.forEach((v) => { mountElement(v, el); }); } else { insert(createText(vnode.children), el); } // 插入到视图内 insert(el, container); }
3. Patch Phase(Dom diff)
数据模型一旦变化渲染函数将再次被调用生成新的虚拟Dom,然后做Dom Diff更新视图Html
function patchProp(el, key, prevValue, nextValue) { // onClick // 1. 如果前面2个值是 on 的话 // 2. 就认为它是一个事件 // 3. on 后面的就是对应的事件名 if (key.startsWith("on")) { const eventName = key.slice(2).toLocaleLowerCase(); el.addEventListener(eventName, nextValue); } else { if (nextValue === null) { el.removeAttribute(key, nextValue); } else { el.setAttribute(key, nextValue); } } }
通过DomDiff - 高效更新视图
网络异常,图片无法展示
|
网络异常,图片无法展示
|
function diff(v1, v2) { // 1. 如果 tag 都不一样的话,直接替换 // 2. 如果 tag 一样的话 // 1. 要检测 props 哪些有变化 // 2. 要检测 children -》 特别复杂的 const { props: oldProps, children: oldChildren = [] } = v1; const { props: newProps, children: newChildren = [] } = v2; if (v1.tag !== v2.tag) { v1.replaceWith(createElement(v2.tag)); } else { const el = (v2.el = v1.el); // 对比 props // 1. 新的节点不等于老节点的值 -> 直接赋值 // 2. 把老节点里面新节点不存在的 key 都删除掉 if (newProps) { Object.keys(newProps).forEach((key) => { if (newProps[key] !== oldProps[key]) { patchProp(el, key, oldProps[key], newProps[key]); } }); // 遍历老节点 -》 新节点里面没有的话,那么都删除掉 Object.keys(oldProps).forEach((key) => { if (!newProps[key]) { patchProp(el, key, oldProps[key], null); } }); } // 对比 children // newChildren -> string // oldChildren -> string oldChildren -> array // newChildren -> array // oldChildren -> string oldChildren -> array if (typeof newChildren === "string") { if (typeof oldChildren === "string") { if (newChildren !== oldChildren) { setText(el, newChildren); } } else if (Array.isArray(oldChildren)) { // 把之前的元素都替换掉 v1.el.textContent = newChildren; } } else if (Array.isArray(newChildren)) { if (typeof oldChildren === "string") { // 清空之前的数据 n1.el.innerHTML = ""; // 把所有的 children mount 出来 newChildren.forEach((vnode) => { mountElement(vnode, el); }); } else if (Array.isArray(oldChildren)) { // a, b, c, d, e -> new // a1,b1,c1,d1 -> old // 如果 new 的多的话,那么创建一个新的 // a, b, c -> new // a1,b1,c1,d1 -> old // 如果 old 的多的话,那么把多的都删除掉 const length = Math.min(newChildren.length, oldChildren.length); for (let i = 0; i < length; i++) { const oldVnode = oldChildren[i]; const newVnode = newChildren[i]; // 可以十分复杂 diff(oldVnode, newVnode); } if (oldChildren.length > length) { // 说明老的节点多 // 都删除掉 for (let i = length; i < oldChildren.length; i++) { remove(oldChildren[i], el); } } else if (newChildren.length > length) { // 说明 new 的节点多 // 那么需要创建对应的节点 for (let i = length; i < newChildren.length; i++) { mountElement(newChildren[i], el); } } } } } }
七、编译器原理
这个地方尤大神并没有实现 后续然叔会给大家提供一个超简洁的版本 这个章节我们主要看看compile这个功能。
网络异常,图片无法展示
|
上文已经说过编译函数的功能
// 编译函数 // 输入值为视图模板 const compile = (template) => { //渲染函数 return (observed, dom) => { // 渲染过程 } }
简单的说就是
- 输入:视图模板
- 输出:渲染函数
细分起来还可以分为三个个小步骤
网络异常,图片无法展示
|
- 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感受一下
vue-next-template-explorer.netlify.com/
网络异常,图片无法展示
|
1. 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}}' } ], })
2. Transform转换处理
前一段知识做的是抽象语法树,对于Vue3模板的特别转换就是在这里进行。
比如:vFor、vOn
在Vue三种也会细致的分为两个层级进行处理
- compile-core 核心编译逻辑
- AST-Parser
- 基础类型解析 v-for 、v-on
网络异常,图片无法展示
|
- compile-dom 针对浏览器的编译逻辑
- v-html
- v-model
- v-clock
网络异常,图片无法展示
|
const transfer = ast => ({ children: [{ tag: 'input', props: { name: 'model', exp: { content: 'message' }, }, }, { tag: 'button', props: { name: 'click', exp: { content: 'message' }, }, children: [{ content: { content: 'message' }, }] } ], })
3. 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 }