前言
这篇文章开始,我们就要继续学习Vue3
中其它包的作用了,在前文也提到过,Vue3
的组成,是有编译时和运行时的概念。
- 编译时:其实就是将模板转化为函数的过程,举个例子,就是将我们写的模板代码,如
<template>{{ msg }}</template>
转化为函数。之所以用模板的方式来写,纯粹是为了减少开发的心智负担,能够根据语义化进行代码书写,而不必用各种函数调用的方式来生成。
- 运行时:运行时又分为两个部分,那么运行时的核心,也就是
runtime-core
是不依赖任何平台的,
那么模块之间的依赖就是runtime-dom
提供了浏览器运行环境中的DOM API
,而runtime-core
提供了虚拟dom
的核心逻辑,通过runtime-dom
提供的API
,从而生成真实DOM
,而runtime-core
中又会引入reactivity
包中的内容,所以整体的流程是Vue -> runtime-dom -> runtime-core -> reactivity
后者均是前者的子级,由前者导入使用。我们本篇文章主要讲运行时相关的内容。
在runtime-core
包中,提供了一个方法createRenderer
,看着虽然陌生,但是在我们项目中的createApp
(在runtime-dom
包中实现),其实底层调用的就是这个方法,那么我们便从这个方法开始,一步步学习runtime-dom
和runtime-core
这两个包吧!
runtime-dom
的实现
首先,我们依旧是要创建文件夹,和之前的套路一样,先看下示例效果,再进行代码书写。和reactivity
包位置相同,我们创建runtime-dom
文件夹和package.json
文件,并且在runtime-dom
文件夹下边创建src/index.ts
作为入口;创建dist/index.html
作为效果展示示例页面。同样,我们把node_modules
文件夹中,Vue
官方打包好的compiler-dom.esm-browser.js
文件,复制进dist
目录下,和我们之前reactivity
的操作一模一样,先看看人家官方的方法实现效果,再自己实现一遍。最后,别忘了将script/dev.js
中的target
改为runtime-dom
,这样,我们就是从runtime-dom/src/index.ts
作为入口进行打包了。万事具备,我们写一下测试代码,看看有没有跑通吧:
1 2
| export const testName = '测试runtime-dom'
|
然后执行npm run dev
,对我们runtime-dom
模块的代码进行打包,之后修改dist/index.html
文件内容,执行npx serve dist
,在浏览器控制台观测结果,成功打印了testName
,便说明我们已经调通了。
1 2 3 4 5
| // runtime-dom/dist/index.html <script type="module"> import { testName } from './runtime-dom.js' console.log(testName) </script>
|
我们首先看下两个方法:createRenderer
和h
的用法。
1 2 3 4 5
| <script type="module">
import { createRenderer, h } from './runtime-dom.esm-browser.js' </script>
|
那么这两个方法,其实是runtime-core
中提供的,前文也说过,其实runtime-dom
提供的主要是浏览器相关的API
,作为参数传入createRenderer
中。什么意思呢?我们一步一步来看。
相信h
方法,大家都有所耳闻,可以生成一个虚拟DOM
,那么调用createRenderer
就可以将虚拟DOM
,通过我们传入的API
,在页面中生成真实的DOM
,我们再次修改示例代码,然后查看控制台结果。
1 2 3 4 5 6 7 8
| <script type="module">
import { createRenderer, h } from './runtime-dom.esm-browser.js' const renderer = createRenderer()
renderer.render(h('h1', 'hello world'), document.getElementById('app')) </script>
|
发现控制台竟然报错了:
代码非常简单,就是想要将h1
标签渲染到页面上,但是为啥报错了呢?我们查看报错的内容,可以发现,提示我们缺少insert
方法,这是啥意思呢?没错,前文提到了runtime-dom
这个包中,提供了DOM
操作的API
,将这些API
配置项等传入createRenderer
,才能够正常的执行代码,所以我们此时要传入一个insert
方法,告诉runtime-core
在将虚拟DOM
转化为真实DOM
,进行插入操作,要用我们传入的这个insert
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <script type="module">
import { createRenderer, h } from './runtime-dom.esm-browser.js'
const renderOptions = { insert (el, container, anchor = null) { container.insertBefore(el, anchor) } }
const renderer = createRenderer(renderOptions)
renderer.render(h('h1', 'hello world'), document.getElementById('app')) </script>
|
此时我们再刷新页面,发现又有了新的报错,很明显,有了前边的经验,我们很容易能明白,原来还缺少一个创建元素的方法,runtime-core
不知道用哪个API
来进行元素的创建,于是我们又补充了一下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <script type="module">
import { createRenderer, h } from './runtime-dom.esm-browser.js'
const renderOptions = { insert (el, container, anchor = null) { container.insertBefore(el, anchor) }, createElement (element) { return document.createElement(element) } }
const renderer = createRenderer(renderOptions)
renderer.render(h('h1', 'hello world'), document.getElementById('app')) </script>
|
我们再运行代码,发现又有了一个报错,还真是没完没了- -,我们不难分析出来,还需要提供一个设置元素值的方法,于是我们再次修改了配置项
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| <script type="module">
import { createRenderer, h } from './runtime-dom.esm-browser.js'
const renderOptions = { insert (el, container, anchor = null) { container.insertBefore(el, anchor) }, createElement (element) { return document.createElement(element) }, setElementText (el, text) { el.innerHTML = text } }
const renderer = createRenderer(renderOptions)
renderer.render(h('h1', 'hello world'), document.getElementById('app')) </script>
|
这次我们再刷新页面,可以发现,页面上终于打印出了hello world
,也就是说,至少要提供创建元素、插入元素、设置元素内容这3个API
,才能够在页面上正常显示一个基本的元素。
说了这么多,大家应该知道runtime-dom
的大致作用了吧?没错,就是提供了上述的这些个renderOptions
中DOM
相关的API
。所以,我们有了大致的思路,便开始实现一下吧!
目录结构如下:
1 2 3 4 5 6
| runtime-dom src module index.ts nodeOps.ts patchProp.ts
|
1 2 3 4 5 6
| // src/index.ts import { nodeOps } from './nodeOps' import { patchProp } from './patchProp'
// 将渲染时所需要的属性做整理 export const renderOptions = Object.assign({ patchProp }, nodeOps)
|
nodeOps.ts文件中,我们存放了和节点相关的操作,不止上文中提到的三个,常见的还有如下一些
API`:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| export const nodeOps = { insert: (child, parent, anchor) => { parent.insertBefore(child, anchor || null); }, remove: child => { const parent = child.parentNode; if (parent) { parent.removeChild(child); } }, createElement: (tag) => document.createElement(tag), createText: text => document.createTextNode(text), setText: (node, text) => node.nodeValue = text, setElementText: (el, text) => el.textContent = text, parentNode: node => node.parentNode, nextSibling: node => node.nextSibling, querySelector: selector => document.querySelector(selector) }
|
除了节点操作,还涉及到了对比属性的方法,比如处理类,样式的替换,事件的绑定解绑,这些都写在src/patchProp.ts
文件中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
import { patchClass } from './module/class' import { patchStyle } from './module/style' import { patchEvent } from './module/event' import { patchAttr } from './module/attr'
export const patchProp = (el, key, prevValue, nextValue) => { if (key === 'class') { patchClass(el, nextValue) } else if (key === 'style') { patchStyle(el, prevValue, nextValue); } else if (/^on[^a-z]/.test(key)) { patchEvent(el, key, nextValue) } else { patchAttr(el, key, nextValue) } }
|
针对不同情况的处理,把这些文件单独放在module
文件夹下
1 2 3 4 5 6 7 8
| export function patchAttr(el, key, value) { if (value == null) { el.removeAttribute(key); } else { el.setAttribute(key, value); } }
|
1 2 3 4 5 6 7 8
| // src/module/class.ts 文件 export function patchClass(el, value) { // 根据最新值设置类名 if (value == null) { el.removeAttribute('class'); } else { el.className = value; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| // src/module/event.ts 文件
function createInvoker(initialValue) { const invoker = (e) => invoker.value(e) // 真实的方法,是绑定在.value上的 invoker.value = initialValue return invoker; } export function patchEvent(el, rawName, nextValue) { const invokers = el._vei || (el._vei = {}) const exisitingInvoker = invokers[rawName] // 是否缓存过
if (nextValue && exisitingInvoker) { // 有新值并且绑定过事件,需要进行换绑操作 exisitingInvoker.value = nextValue; } else { // 获取注册事件的名称 const name = rawName.slice(2).toLowerCase() if (nextValue) {// 缓存函数 const invoker = (invokers[rawName]) = createInvoker(nextValue) el.addEventListener(name, invoker); } else if (exisitingInvoker) { el.removeEventListener(name, exisitingInvoker); invokers[rawName] = undefined } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| // src/module/style.ts 文件 export function patchStyle(el, prev, next) { // 更新style const style = el.style; for (const key in next) { // 用最新的直接覆盖 style[key] = next[key] } if (prev) { for (const key in prev) {// 老的有新的没有删除 if (next[key] == null) { style[key] = null } } } }
|
那么有了这些个API
,runtime-core
就知道,应该用哪些方法将虚拟DOM
转化为真实DOM
了。之后,我们引入自己的renderOptions
看看能不能正常渲染:
1 2 3 4 5 6
| <script type="module"> import { createRenderer, h } from './runtime-dom.esm-browser.js' import { renderOptions } from './runtime-dom.js' const renderer = createRenderer(renderOptions) renderer.render(h('h1', 'hello'), app) </script>
|
页面正常渲染了!那么针对上文这种方式,适合针对某个平台(跨平台),自己定义一套渲染API
,可以随意进行定制化,如果在浏览器环境下,其实正如上文所说,API
都已经在runtime-dom
中了,所以在内部又提供了一个方法(render
),默认把这一坨renderOptions
自动传进去了,不用我们再手动传入:
1 2 3 4 5 6 7
| <script type="module"> import { createRenderer, h, render } from './runtime-dom.esm-browser.js'
render(h('h1', 'hello'), app) </script>
|
再次运行代码,发现结果没变,还是能正常运行,说明这两种方式都可行,使用render
的话,相当于默认传入浏览器环境下的API
,使用createRenderer
可以自定义传入API
,比较灵活,所以,我们最后还需要改一下入口文件的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import { nodeOps } from './nodeOps' import { patchProp } from './patchProp' import { createRenderer as renderer } from '@vue/runtime-core'
export const renderOptions = Object.assign({ patchProp }, nodeOps)
export function createRenderer (options) { return renderer(options) }
export function render (vnode, container) { const renderer = createRenderer(renderOptions) return renderer.render(vnode, container) }
export * from '@vue/runtime-core'
|
诶,这样就没啥问题了,既然从runtime-core
包中引入了渲染的方法,那么接下来我们需要的就是来实现runtime-core
的核心逻辑了。
runtime-core
的实现
老规矩,首先还是创建相应文件夹:
1 2 3 4 5 6
| runtime-core src index.ts createVNode.ts renderer.ts h.ts
|
先在入口文件进行导出操作,防止后边忘记掉,我们不难发现,正如我们之前所说,runtime-dom
将DOM
相关API
传给runtime-core
,runtime-core
中又使用了reactivity
模块,至此,三个模块便互相串通了起来。
1 2 3 4 5
| export * from './renderer' export * from './createVNode' export * from './h' export * from '@vue/reactivity'
|
我们还是先紧跟着runtime-dom
的逻辑,先写下renderer.ts
的大概逻辑,那么这个就是我们runtime-dom
中使用的createRenderer
方法,实际上调用的还是runtime-core
中的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| export function createRenderer(renderOptions) { const { insert: hostInsert, remove: hostRemove, patchProp: hostPatchProp, createElement: hostCreateElement, createText: hostCreateText, setText: hostSetText, setElementText: hostSetElementText, parentNode: hostParentNode, nextSibling: hostNextSibling, querySelector: hostQuerySelector } = renderOptions const render = (vnode, container) => { console.log('render') } return { render } }
|
接下来我们写一下createVNode.ts
中的逻辑。所谓虚拟DOM
,就是用对象的形式,来形容一个节点,标注了各种信息,为了之后转化成真实DOM
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| import { ShapeFlags } from '@vue/shared'
export function isVNode(value) { return value ? value.__v_isVNode === true : false } export function createVNode(type, props, children = null) { const shapeFlag = typeof type === 'string' ? ShapeFlags.ELEMENT : 0 const vnode = { __v_isVNode: true, type, props, key: props && props['key'], el: null, children, shapeFlag } if (children) { let type = 0; if (Array.isArray(children)) { type = ShapeFlags.ARRAY_CHILDREN; } else { children = String(children); type = ShapeFlags.TEXT_CHILDREN } vnode.shapeFlag |= type } return vnode }
|
我们在@vue/shared
包中补充下ShapFlags
,并且来详细解释一下,这到底是个什么东西。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| export const enum ShapeFlags { ELEMENT = 1, FUNCTIONAL_COMPONENT = 1 << 1, STATEFUL_COMPONENT = 1 << 2, TEXT_CHILDREN = 1 << 3, ARRAY_CHILDREN = 1 << 4, SLOTS_CHILDREN = 1 << 5, TELEPORT = 1 << 6, SUSPENSE = 1 << 7, COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8, COMPONENT_KEPT_ALIVE = 1 << 9, COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT }
|
很多朋友可能对<< | &
移位、按位或、按位与很陌生,就算知道其定义,也不知道有哪些个使用场景。其实在Vue3
中,就有很好的例子。比如这个ShapFlags
通过名称我们便能大致猜出来,是描述形状的标志,比如一个普通的元素,就用1来代表,函数式组件就用1向左移1位来表示,普通的状态组件,就用1向左移2位来表示。为啥要用移位操作呢?搞几个普通的枚举值不行么,其实,之所以用移位来进行标识,是为了后续进行按位与,按位或操作提供了极大的便利。我们举个例子,我们有如下的3种权限:
1 2 3
| 测试:1,二进制为001 开发者:1 << 1,二进制为010 超级管理员:1 << 2,二进制为100
|
那么当A
,既是开发者
,又是超级管理员
的时候,那么只需要将开发者
和超级管理员
的权限进行按位或操作,也就是010
和100
进行按位或操作,得到的结果为110
,大于0
。那么判断A
有没有测试
权限,只需要将刚才的结果和测试
的权限进行按位与操作,即110
和001
进行按位与操作,得到的结果为000
,等于0
。从而我们可以发现,在使用移位符操作的枚举值,进行|
操作后,相当于权限相加的操作,进行&
操作后,如果结果大于0
,说明包含相关权限,如果结果等于0
,则说明不包括相关权限。
那么再回到我们之前的实例,我们改动下index.html
中的代码,来调试下代码有没有生效,先调试下createVNode
方法:
1 2 3 4
| <script> import { createVNode } from './runtime-dom.js' console.log(createVNode('div',null, ['hello', 'world'])) </script>
|
打印结果可以看到,虚拟节点成功的被创建了:
但是createVNode
这个方法,写法是固定的,比如传参的顺序和类型,都不能变,并不灵活,(特别注意,createVNode
的第三个参数,只能传字符串和数组类型的数据),所以,我们可以基于createVNode
进行封装,那么这个方法就是我们熟悉的h
方法了,首先我们先看下h
方法能怎么传参:
- 只传1个参数,就是标签;
- 传2个参数,可能是传标签和属性:
h('div', { style: { color: 'red' } })
,也可能是传标签和子元素:h('div', h('span', null, 'hello')) h('div', [h('span', null, 'hello')])
,还可能是传标签和内容:h('div', 'hello')
- 传3个参数,那就是和
createVNode
的传参一样了,即h('div', { style: {color: 'red'} }, 'hello')
- 传3个以上的参数,第二个参数必须是属性,之后的参数都作为内容:
h('div', null, 'hello', 'world', '!')
那么知道了以上的用法,我们便可以按照传参数量的不同,分别处理相应逻辑,来编写h
方法了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| import { isArray, isObject } from '@vue/shared' import { createVNode, isVNode } from './createVNode'
export function h(type, propsOrChildren?, children?) { const l = arguments.length if (l === 2) { if (isObject(propsOrChildren) && !isArray(propsOrChildren)) { if (isVNode(propsOrChildren)) { return createVNode(type, null, [propsOrChildren]) } return createVNode(type, propsOrChildren) } else { return createVNode(type, null, propsOrChildren) } } else { if (l > 3) { children = Array.prototype.slice.call(arguments, 2) } else if (l === 3 && isVNode(children)) { children = [children] } return createVNode(type, propsOrChildren, children) } }
|
到此,我们h
方法便写好了,是不是没有想象中那么困难呢?
接下来,就该完善createRenderer
方法,也就是渲染方法了,之后二者一结合,就能够在页面中,将虚拟DOM
渲染成真实DOM
了,刚才我们写到了render
方法,那我们继续完善吧!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| export function createRenderer(renderOptions) { const { insert: hostInsert, remove: hostRemove, patchProp: hostPatchProp, createElement: hostCreateElement, createText: hostCreateText, setText: hostSetText, setElementText: hostSetElementText, parentNode: hostParentNode, nextSibling: hostNextSibling, querySelector: hostQuerySelector } = renderOptions const render = (vnode, container) => { if (vnode == null) { }else { patch(container._vnode || null, vnode, container) } container._vnode = vnode } return { render } } const mountElement = (vnode, container) => { }
const patch = (n1, n2, container) => { if(n1 == n2) { return } if(n1 == null) { mountElement(n2, container) }else { } }
|
那么整个流程的架子我们已经搭好了,接下来就一个个来实现具体的方法,我们先实现将虚拟DOM
转化为真实DOM
的mountElement
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| export function createRenderer(renderOptions) { const { insert: hostInsert, remove: hostRemove, patchProp: hostPatchProp, createElement: hostCreateElement, createText: hostCreateText, setText: hostSetText, setElementText: hostSetElementText, parentNode: hostParentNode, nextSibling: hostNextSibling, querySelector: hostQuerySelector } = renderOptions const mountChildren = (children, container) => { for(let i = 0; i < children.length; i++) { patch(null,children[i],container) } } const mountElement = (vnode, container) => { const { type, props, shapeFlag, children } = vnode let el = vnode.el = hostCreateElement(type) if (props) { for (const key in props) { hostPatchProp(el, key, null, props[key]) } } if(children) { if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { hostSetElementText(el, vnode.children) }else if(shapeFlag & ShapeFlags.ARRAY_CHILDREN) { mountChildren(vnode.children, el) } } hostInsert(el,container); } const patch = (n1, n2, container) => { if(n1 == n2) { return } if(n1 == null) { mountElement(n2, container) }else { } } const render = (vnode, container) => { if (vnode == null) { }else { patch(container._vnode || null, vnode, container) } container._vnode = vnode } return { render } }
|
我们可以清楚的看到,就是用了runtime-dom
中的API
,来递归生成真实的DOM
元素,我们来验证一下,代码是否有问题吧:
1 2 3 4
| <script type="module"> import { h, render } from './runtime-dom.js' render(h('h1',{style: {color: 'red'}}, 'hello'), app) </script>
|
在浏览器中运行完代码,发现,hello
已经成功被渲染到页面上了。那么初始化阶段的渲染的逻辑,便写完了!那么初始化逻辑写完后,我们再写一下卸载的逻辑,什么是卸载的逻辑呢?可以理解为render(null, app)
,也就是传入了null
的时候,要把页面中元素清除掉,我们之前已经预留出来卸载逻辑的位置,那我们现在便可以来完善了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
| export function createRenderer(renderOptions) { const { insert: hostInsert, remove: hostRemove, patchProp: hostPatchProp, createElement: hostCreateElement, createText: hostCreateText, setText: hostSetText, setElementText: hostSetElementText, parentNode: hostParentNode, nextSibling: hostNextSibling, querySelector: hostQuerySelector } = renderOptions const mountChildren = (children, container) => { for(let i = 0; i < children.length; i++) { patch(null,children[i],container) } } const mountElement = (vnode, container) => { const { type, props, shapeFlag, children } = vnode let el = vnode.el = hostCreateElement(type) if (props) { for (const key in props) { hostPatchProp(el, key, null, props[key]) } } if(children) { if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { hostSetElementText(el, vnode.children) }else if(shapeFlag & ShapeFlags.ARRAY_CHILDREN) { mountChildren(vnode.children, el) } } hostInsert(el,container); } const patch = (n1, n2, container) => { if(n1 == n2) { return } if(n1 == null) { mountElement(n2, container) }else { } } const unmount = vnode => { const { shapeFlag } = vnode if(shapeFlag & ShapeFlags.ELEMENT) { hostRemove(vnode.el) } } const render = (vnode, container) => { if (vnode == null) { if(container._vnode) { unmount(container._vnode) } }else { patch(container._vnode || null, vnode, container) } container._vnode = vnode } return { render } }
|
到这里,就只剩下元素更新的逻辑了,那么元素更新的逻辑,涉及的内容又非常多,我们先讲一些关键性的点,从而为后续文章做好铺垫。我们用几个不同的例子,来表明什么时候触发更新,也就是说,怎么判断两个虚拟节点相同,可以复用:
1 2 3 4 5 6 7 8 9 10 11 12
| <script type="module"> import { h, render } from './runtime-dom.js' render(h('h1',{style: {color: 'red'}}, 'hello'), app) render(h('div',{style: {color: 'blue'}}, 'world'), app) render(h('div',{style: {color: 'blue'}, key: 1}, 'world'), app) render(h('div',{style: {color: 'blue'}, key: 2}, 'world'), app) render(h('h1',{style: {color: 'red'}}, 'hello'), app) render(h('h1',{style: {color: 'red'}}, 'hello'), app) </script>
|
所以我们可以得到结论,当两个虚拟节点的标签类型不同时候,或者两个虚拟节点标签类型相同,但是key不同,那么就不会进行复用;如果两个虚拟节点的标签类型相同,并且不传入key,或者key相同,那么就进行复用。
所以,我们需要继续改进下更新下patch
方法中的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
| export function createRenderer(renderOptions) { const { insert: hostInsert, remove: hostRemove, patchProp: hostPatchProp, createElement: hostCreateElement, createText: hostCreateText, setText: hostSetText, setElementText: hostSetElementText, parentNode: hostParentNode, nextSibling: hostNextSibling, querySelector: hostQuerySelector } = renderOptions const mountChildren = (children, container) => { for(let i = 0; i < children.length; i++) { patch(null,children[i],container) } } const mountElement = (vnode, container) => { const { type, props, shapeFlag, children } = vnode let el = vnode.el = hostCreateElement(type) if (props) { for (const key in props) { hostPatchProp(el, key, null, props[key]) } } if(children) { if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { hostSetElementText(el, vnode.children) }else if(shapeFlag & ShapeFlags.ARRAY_CHILDREN) { mountChildren(vnode.children, el) } } hostInsert(el,container); } const isSameVNode = (n1, n2) => { return n1.type === n2.type && n1.key === n2.key } const processElement = (n1, n2, container) => { if (n1 == null) { mountElement(n2, container) } else { console.log(n1, n2); } } const patch = (n1, n2, container) => { if(n1 == n2) { return } if(n1 && !isSameVNode(n1, n2)) { unmount(n1) n1 = null } processElement(n1, n2, container) } const unmount = vnode => { const { shapeFlag } = vnode if(shapeFlag & ShapeFlags.ELEMENT) { hostRemove(vnode.el) } } const render = (vnode, container) => { if (vnode == null) { if(container._vnode) { unmount(container._vnode) } }else { patch(container._vnode || null, vnode, container) } container._vnode = vnode } return { render } }
|
我们又将当两个虚拟节点不相同时的更新逻辑写完了,我们改下调试代码,在页面上看效果,发现,过了1秒钟后,成功的渲染了新的虚拟节点:
1 2 3 4 5 6 7
| <script type="module"> import { h, render } from './runtime-dom.js' render(h('h1', { style: { color: 'red' } }, 'hello'), app) setTimeout(() => { render(h('div', { style: { color: 'blue' } }, 'world'), app) }, 2000) </script>
|
所以,当两个虚拟节点可以复用时的逻辑,我们就放到后续文章中,进行详细的讲解,因为会涉及到我们耳熟能详的diff
算法。
作者:柠檬soda水
链接:https://juejin.cn/post/7209906040921489468
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。