渲染器与响应系统的结合
给effect传入一个renderer,renderer里面读取了响应式类型的数据,就会使响应式数据跟响应式数据关联起来,当响应式类型数据改变时自动触发renderer查询渲染dom
js
function renderer(domString, container) {
container.innerHTML = domString
}
const count1 = ref(1)
effect(() => {
renderer(`<h1>${count1.value}</h1>`,document.getElementById('app'))
})
count1.value++
渲染器的基本概念
挂载
createRenderer函数返回render,render函数会以container为挂载点,将vnode渲染为真实DOM并添加到该挂载点下
js
function createRenderer() {
function render(vnode, container) {
}
return {
render
}
}
const renderer = createRenderer()
// 挂载
renderer.render(oldVNode, document.querySelector('#app'))
更新
如果多次调用render进行更新操作,就需要新vnode跟旧vnode进行比较,提高性能
js
const renderer = createRenderer()
// 首次渲染
renderer.render(oldVNode, document.querySelector('#app'))
// 第二次渲染
renderer.render(newVNode, document.querySelector('#app'))
解决思路:
- 如果vnode存在就说明有新的vnode就要传入patch进行打补丁(更新)
- 如果vnode不存在就说明没有新的vnode并且旧的vnode存在,说明需要把旧的vnode清空
- 将新的vnode保存到container的_vnode下,如果多次调用render并且container相同就可以以container._vnode为旧vnode,vnode为新vnode
diff
function createRenderer() {
function render(vnode, container) {
+ // 新 vnode 存在,将其与旧 vnode 一起传递给 patch 函数,进行打补丁
+ if(vnode) {
+ patch(container._vnode, vnode, container)
+ }else {
+ // 旧 vnode 存在,且新 vnode 不存在,说明是卸载(unmount)操作
+ if (container._vnode) {
+ // 只需要将 container 内的 DOM 清空即可
+ container.innerHTML = ""
+ }
+ }
+ // 把 vnode 存储到 container._vnode 下,即后续渲染中的旧 vnode
+ container._vnode = vnode
}
return {
render
}
}
自定义渲染器
判断oldN也就是旧的vnode,不存在就意味着挂载就调用mountElement
js
function patch(oldN, newN, container) {
// 如果 oldN 不存在,意味着挂载,则调用 mountElement 函数完成挂载
if (!oldN) {
// 挂载
mountElement(newN, container)
} else {
// 暂不操作
}
}
- 根据vnode的type来创建对应的type
- 判断vnode的children是什么节点,如果是文本节点就使用textContent设置
- 最后使用appendChild添加到container中
js
function mountElement(vnode, container) {
// 创建 DOM 元素
const el = document.createElement(vnode.type)
// 处理子节点,如果子节点是字符串,代表元素具有文本节点
if (typeof vnode.children === "string") {
// 因此只需要设置元素的 textContent 属性即可
el.textContent = vnode.children
}
// 将元素添加到容器中
container.appendChild(el)
}
指定自定义的配置对象
因为mountElement 函数内调用了大量createElement、textContent、appendChild等浏览器api,需要将这些api抽离成配置项传入createRenderer,使createRenderer作用域内定义的函数都可以访问,只要传入不同的配置项,就能够完成非浏览器环境下的渲染工作。
diff
-function createRenderer() {
+function createRenderer(options) {
// 通过 options 得到操作 DOM 的 API
+ const { createElement, insert, setElementText } = options
// 在这个作用域内定义的函数都可以访问那些 API
function mountElement(vnode, container) {
// ...
}
function patch(n1, n2, container) {
// ...
}
function render(vnode, container) {
// ...
}
return {
render
}
}
diff
function mountElement(vnode, container) {
// 调用 createElement 函数创建元素
- const el = document.createElement(vnode.type)
+ const el = createElement(vnode.type)
if (typeof vnode.children === 'string') {
// 调用 setElementText 设置元素的文本节点
- el.textContent = vnode.children
+ setElementText(el, vnode.children)
}
- container.appendChild(el)
// 调用 insert 函数将元素插入到容器内
+ insert(el, container)
}
使用浏览器api执行
js
// 在创建 renderer 时传入配置项
const renderer = createRenderer({
// 用于创建元素
createElement(tag) {
console.log(`创建元素 ${tag}`)
return document.createElement(tag)
}, // 用于设置元素的文本节点
setElementText(el, text) {
console.log("设置",el,"的文本内容:",text)
el.textContent = text
},
// 用于在给定的 parent 下添加指定元素
insert(el, parent, anchor = null) {
console.log("将",el,"添加到",parent,"下")
parent.insertBefore(el, anchor)
}
})
const vnode = {
type: 'h1',
children: 'hello'
}
const container = document.getElementById("app")
renderer.render(vnode, container)
不使用浏览器api
既可以在浏览器使用也可以在node中使用
js
// 在创建 renderer 时传入配置项
const renderer = createRenderer({
// 用于创建元素
createElement(tag) {
console.log(`创建元素 ${tag}`)
return { tag }
}, // 用于设置元素的文本节点
setElementText(el, text) {
console.log(`设置 ${JSON.stringify(el)} 的文本内容:${text}`)
el.textContent = text
},
// 用于在给定的 parent 下添加指定元素
insert(el, parent, anchor = null) {
console.log(`将 ${JSON.stringify(el)} 添加到${JSON.stringify(parent)} 下`)
parent.children = el
}
})
const vnode = {
type: 'h1',
children: 'hello'
}
const container = { type: 'root' }
renderer.render(vnode, container)
console.log(container)