如果是组件则:vnode .type的值是一个对象。
如下:
const vnode = {
type: MyComponent,
}
为了让渲染器
能处理组件类型的虚拟节点,我们还需要在patch函数中对组件类型的虚拟节点进行处理,如下:
function patch(n1, n2, container, anchor) {
if(!n1 && n1.type !== n2.type) {
unmount(n1)
n1 = nill
}
const { type } = n2
if (typeof type === 'string') {
} else if (typeof type === 'object') {
// 组件
if (!n1) {
// 挂载组件
+ mountComponent(n2, container,anchor )
} else {
// 更新组件
+ patchComponent(n1, n2, anchor)
}
}
}
一个组件必须包含一个渲染函数,即render函数,并且渲染函数的返回值应该是虚拟dom。如下:
const MyComponent = {
name: 'MyComponent',
render() {
return {
type: 'div',
children: '我是文本'
}
}
}
有了基本的结构,渲染器就能完成组件的渲染。渲染器中真正完成组件的渲染的是mountComponent函数。实现如下:
function mountComponent(vnode, container, anchor) {
// 通过vnode获取组件的选项对象,即vnode.type
const componentOptions = vnode.type
// 获取组件的渲染函数
const { render } = componentOptions
// 执行渲染函数,获取组件的渲染函数内容,即render返回的虚拟dom
const subTree = render()
// 最后调用 patch 函数来挂载组件所描述的内容,即 subTree
patch(null, subTree, container, anchor)
}
为组件设计自身的状态:data
我们用data函数来定义组件自身的状态。
const MyComponent = {
name: 'MyComponent',
data() {
return {
foo: 'dd'
}
},
render() {
return {
type: 'div',
children: `foo 的值是: ${this.foo}` // 在渲染函数内使用组件状态
}
}
}
我们约定用户必须使用data函数来定义组件自身的状态,同时可以在渲染函数中通过this访问data函数返回的状态数据:
function mountComponent(vnode, container, anchor) {
// 通过vnode获取组件的选项对象,即vnode.type
const componentOptions = vnode.type
// 获取组件的渲染函数
+ const { render, data } = componentOptions
+ //调用 data 函数得到原始数据,并调用 reactive 函数将其包装为响应式数据
+ const state = reactive(data())
// 执行渲染函数,获取组件的渲染函数内容,即render返回的虚拟dom
+ // 调用 render 函数时,将其 this 设置为 state,从而 render 函数内部可以通过 this 访问组件自身状态数据
+ const subTree = render.call(state,state)
// 最后调用 patch 函数来挂载组件所描述的内容,即 subTree
patch(null, subTree, container, anchor)
}
实现组件自身状态的初始化需要两个步骤:
当组件自身状态发生变化时,我们需要有能力触发组件更新,即 组件的自更新。
为此,我们需要将整个渲染任务包装到一个effect中,如下:
function mountComponent(vnode, container, anchor) {
// 通过vnode获取组件的选项对象,即vnode.type
const componentOptions = vnode.type
// 获取组件的渲染函数
const { render, data } = componentOptions
//调用 data 函数得到原始数据,并调用 reactive 函数将其包装为响应式数据
const state = reactive(data())
+ // 将组件的 render 函数调用包装到 effect 内
+ effect(() => {
// 执行渲染函数,获取组件的渲染函数内容,即render返回的虚拟dom
// 调用 render 函数时,将其 this 设置为 state,从而 render 函数内部可以通过 this 访问组件自身状态数据
const subTree = render.call(state,state)
// 最后调用 patch 函数来挂载组件所描述的内容,即 subTree
patch(null, subTree, container, anchor)
+ })
}
将组件的 render 函数调用包装到 effect 内,这样一旦组件自身响应式数据发生变化,组件就会自动重新 执行渲染函数,从而完成更新。但是,由于effect的执行是同步的,因此放响应式数据发生变化时,与之关联的副作用函数会同步执 行。
换句话说,如果多次修改响应式数据的值,将会导致渲染函数执 行多次,这实际上是没有必要的。因此,我们需要设计一个机制,以 使得无论对响应式数据进行多少次修改,副作用函数都只会重新执行 一次。为此,我们需要实现一个调度器,当副作用函数需要重新执行 时,我们不会立即执行它,而是将它缓冲到一个微任务队列中,等到 执行栈清空后,再将它从微任务队列中取出并执行。有了缓存机制,我们就有机会对任务进行去重,从而避免多次执行副作用函数带来的性能开销。
具体实现如下:
// 任务缓存队列,用一个 Set 数据结构来表示,这样就可以自动对任务进行去重
const queue = new Set()
// 一个标志,代表是否正在刷新任务队列
let isFlushing = false
// 创建一个立即 resolve 的 Promise 实例
const p = Promiser.resolve()
// 调度器的主要函数,用来将一个任务添加到缓冲队列中,并开始刷新队列
function queueJob(job) {
queue.add(job)
// 如果还没有开始刷新队列,则刷新之
if (!isFlushing) {
isFlushing = true
p.then(() => {
try {
// 执行任务队列中的任务
queue.forEach((job) => job())
} finally{
// 重置状态
isFlushing = false
queue.clear = 0
}
})
}
}
上面是调度器的最小实现,本质上利用了微任务的异步执行机 制,实现对副作用函数的缓冲。其中 queueJob 函数是调度器最主要 的函数,用来将一个任务或副作用函数添加到缓冲队列中,并开始刷 新队列
。有了 queueJob 函数之后,我们可以在创建渲染副作用时使 用它,
function mountComponent(vnode, container, anchor) {
// 通过vnode获取组件的选项对象,即vnode.type
const componentOptions = vnode.type
// 获取组件的渲染函数
const { render, data } = componentOptions
//调用 data 函数得到原始数据,并调用 reactive 函数将其包装为响应式数据
const state = reactive(data())
// 将组件的 render 函数调用包装到 effect 内
effect(() => {
// 执行渲染函数,获取组件的渲染函数内容,即render返回的虚拟dom
// 调用 render 函数时,将其 this 设置为 state,从而 render 函数内部可以通过 this 访问组件自身状态数据
const subTree = render.call(state,state)
// 最后调用 patch 函数来挂载组件所描述的内容,即 subTree
patch(null, subTree, container, anchor)
}, {
// 指定该副作用函数的调度器为 queueJob 即可
scheduler: queueJob
})
}
这样,当响应式数据发生变化时,副作用函数不会立即同步执行,而是会被 queueJob 函数调度,最后在一个微任务中执行。
不过,上面这段代码存在缺陷。可以看到,我们在 effect 函数内调用 patch 函数完成渲染时,第一个参数总是 null。这意味着,每次更新发生时都会进行全新的挂载,而不会打补丁,这是不正确的。正确的做法是:每次更新时,都拿新的 subTree 与上一次组件所渲染的 subTree 进行打补丁。为此,我们需要实现组件实例,用它来维护组件整个生命周期的状态,这样渲染器才能够在正确的时机执行合适的操作。
组件实例本质上是一个状态集合(对象)。
引入组件实例
function mountComponent(vnode, container, anchor) {
// 通过vnode获取组件的选项对象,即vnode.type
const componentOptions = vnode.type
// 获取组件的渲染函数
const { render, data } = componentOptions
//调用 data 函数得到原始数据,并调用 reactive 函数将其包装为响应式数据
const state = reactive(data())
+ const instance = {
+ state, // 组件自身的状态数据,即 data
+ isMounted: false, // 一个布尔值,用来表示组件是否已经被挂载,初始值为 false
+ subTree: null // 组件所渲染的内容,即子树(subTree)
+ }
// 将组件实例设置到 vnode 上,用于后续更新
+ vnode.component = instance
// 将组件的 render 函数调用包装到 effect 内
effect(() => {
// 执行渲染函数,获取组件的渲染函数内容,即render返回的虚拟dom
// 调用 render 函数时,将其 this 设置为 state,从而 render 函数内部可以通过 this 访问组件自身状态数据
const subTree = render.call(state,state)
// // 检查组件是否已经被挂载
+ if (!isMounted) {
// 初次挂载,调用 patch 函数第一个参数传递 null
+ patch(null, subTree, container, anchor)
+ // 将组件实例的isMounted设置为true,这样当更新发生时就不会再次进行挂载操作。而是执行更新
+ instance.isMounted = true
+ } else {
+ // 当isMounted为true时,说明组件已经挂载了,只需要完成自更新即可
+ patch(instance.subTree,subTree, conatiner, anchor)
+ }
// 最后调用 patch 函数来挂载组件所描述的内容,即 subTree
+ patch(null, subTree, container, anchor)
+ // 更新组件实例的子树
+ instance.subTree = subTree
}, {
// 指定该副作用函数的调度器为 queueJob 即可
scheduler: queueJob
})
}
在上面这段代码中,我们使用一个对象来表示组件实例,该对象有三个属性。
在上面的实现中,组件实例的 instance.isMounted 属性可以 用来区分组件的挂载和更新。
function mountComponent(vnode, container, anchor) {
// 通过vnode获取组件的选项对象,即vnode.type
const componentOptions = vnode.type
// 从组件选项对象中取得组件的生命周期函数
+ const { render, data, beforeCreate, created, beforeMount,
mounted, beforeUpdate, updated } = componentOptions
// 在这里调用beforeCreate钩子
beforeMount && beforeMount()
const state = reactive(data())
const instance = {
state,
isMounted: false,
subTree: null
}
vnode.component = instance
// 在这里调用 created 钩子
created && created(state)
effect(() => {
const subTree = render.call(state, state)
if (!instance.isMounted) {
beforeMount && beforeMount.call(state)
}
})
}