1.渲染组件
如果是组件则: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 } = n2if (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.typeconst componentOptions = vnode.type// 获取组件的渲染函数const { render } = componentOptions// 执行渲染函数,获取组件的渲染函数内容,即render返回的虚拟domconst subTree = render()// 最后调用 patch 函数来挂载组件所描述的内容,即 subTreepatch(null, subTree, container, anchor)
}
2.组件状态与自更新
为组件设计自身的状态: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.typeconst 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 函数来挂载组件所描述的内容,即 subTreepatch(null, subTree, container, anchor)
}
实现组件自身状态的初始化需要两个步骤:
-
- 通过组件的选项对象取得 data 函数并执行,然后调用 reactive 函数将 data 函数返回的状态包装为响应式数据;
-
- 在调用 render 函数时,将其 this 的指向设置为响应式数据 state,同时将 state 作为 render 函数的第一个参数传递。
当组件自身状态发生变化时,我们需要有能力触发组件更新,即 组件的自更新。
为此,我们需要将整个渲染任务包装到一个effect中,如下:
function mountComponent(vnode, container, anchor) {// 通过vnode获取组件的选项对象,即vnode.typeconst 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 函数来挂载组件所描述的内容,即 subTreepatch(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 = truep.then(() => {try {// 执行任务队列中的任务queue.forEach((job) => job())} finally{// 重置状态isFlushing = falsequeue.clear = 0}})}
}
上面是调度器的最小实现,本质上利用了微任务的异步执行机 制,实现对副作用函数的缓冲。其中 queueJob 函数是调度器最主要 的函数,用来将一个任务或副作用函数添加到缓冲队列中,并开始刷 新队列
。有了 queueJob 函数之后,我们可以在创建渲染副作用时使 用它,
function mountComponent(vnode, container, anchor) {// 通过vnode获取组件的选项对象,即vnode.typeconst 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 函数来挂载组件所描述的内容,即 subTreepatch(null, subTree, container, anchor)}, {// 指定该副作用函数的调度器为 queueJob 即可scheduler: queueJob})}
这样,当响应式数据发生变化时,副作用函数不会立即同步执行,而是会被 queueJob 函数调度,最后在一个微任务中执行。
不过,上面这段代码存在缺陷。可以看到,我们在 effect 函数内调用 patch 函数完成渲染时,第一个参数总是 null。这意味着,每次更新发生时都会进行全新的挂载,而不会打补丁,这是不正确的。正确的做法是:每次更新时,都拿新的 subTree 与上一次组件所渲染的 subTree 进行打补丁。为此,我们需要实现组件实例,用它来维护组件整个生命周期的状态,这样渲染器才能够在正确的时机执行合适的操作。
3.组件实例与组件的生命周期
组件实例本质上是一个状态集合(对象)。
引入组件实例
function mountComponent(vnode, container, anchor) {// 通过vnode获取组件的选项对象,即vnode.typeconst 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})}
在上面这段代码中,我们使用一个对象来表示组件实例,该对象有三个属性。
- state:组件自身的状态数据,即 data。
- isMounted:一个布尔值,用来表示组件是否被挂载。
- subTree:存储组件的渲染函数返回的虚拟 DOM,即组件的子树 (subTree)。
在上面的实现中,组件实例的 instance.isMounted 属性可以 用来区分组件的挂载和更新。
function mountComponent(vnode, container, anchor) {// 通过vnode获取组件的选项对象,即vnode.typeconst 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)}}) }