Vue的设计与实现——组件的实现原理(p1)

2024/9/12 Vue源码组件

在编写比较复杂的页面的时候,用来描述页面结构的虚拟DOM的代码量会变得越来越多.这时候我们就需要用组件来封装一些公共的结构,这样在页面中就可以复用这些公共的结构了.

# 渲染组件

渲染器会使用虚拟节点的type属性来区分其属性,对与不同节点类型的节点,需要不同的方法进行挂载和更新.

对于组件来说也是一样的,比如:

const vnode = {
    type: MyComponent
    //....
}
1
2
3
4

同样的,我们需要在patch函数中新开一个分支处理.

function patch(n1, n2, container, anchor) {
    if(n1 && n1.type !== n2.type) {
        unmount(n1)
        n1 =null
    }
    const { type } = n2
    if(typeof type === 'string') {
        //
    } else if(type === Text) {
        //...
    } else if(type == Fragment) {
        //...
    } else if(typeof type === 'object') {
        //作为组件处理
        if(!n1) {
            mountComponent(n2, container, anchor)
        } else {
            //更新组件
            updateComponent(n1, n2,anchor)
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

渲染器有能力处理组件后,我们要设计组件在用户层面的接口.一个组件必须包含一个渲染函数(render函数),并且渲染的返回应该是虚拟DOM.

const MyComponent = {
    name: 'MyComponent',
    render() {
        return {
            type: 'div',
            children: `我是文本内容`
        }
    }
}
1
2
3
4
5
6
7
8
9

渲染器中真正完成组件渲染任务的是mountComponent函数,具体实现如下所示

function mountComponent(vnode, container, anchor) {
    //通过vnode获取组件的选项对象
    const componentOptions = vnode.type
    //获取组件的render函数
    const { render } = componentOptions
    //执行渲染函数,获取组件要渲染的内容(render函数返回的虚拟DOM)
    const subTree = render()
    // 调用patch函数挂载
    patch(null, subTree, container, anchor)
}
1
2
3
4
5
6
7
8
9
10

这样我们就实现了最基本的组件化方案.

# 组件状态与自更新

上一节我们完成了组件的初始渲染,接下来我们尝试为组件设计自身的状态

const MyComponent = {
    name: 'MyComponent',
    //用data函数来定义组件自身的状态
    data() {
        return {
            foo: 'hello world'
        }
    },
    render() {
        return {
            type: 'div',
            children: this.foo //在渲染函数内使用组件状态
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

我们约定必须使用data函数来定义组件自身的状态,同时可以在渲染函数中通过this访问由data函数返回的状态数据.
下面代码实现了组件自身状态的初始化

function mountComponent(vnode, container, anchor) {
    const componentOptions = vnode.type
    const { data, render } = componentOptions
    //得到原始数据并且用reactive包裹为响应式数据
    const state = reactive(data())
    // 调用render函数的时候,将this设为state,并且将state作为第一个参数传入
    // 从而render函数内部可以通过this访问组件自身状态数据
    const subTree = render.call(state,state)
    patch(null, subTree, container, anchor)
}
1
2
3
4
5
6
7
8
9
10

当组件自身发生变化的时候,我们需要有能力触发组件的更新.即组件的自更新,为此我们需要将整个渲染任务包装到一个effect中.

function mountComponent(vnode, container, anchor) {
    const componentOptions = vnode.type
    const { data, render } = componentOptions
    const state = reactive(data())
    // 将组件的render函数包装在effect内
    effect(() => {
        const subTree = render.call(state,state)
        patch(null, subTree, container, anchor)
    })
}
1
2
3
4
5
6
7
8
9
10

这样,一旦组件自身的响应式数据发生变化,组件就会自动重新执行渲染函数.但是,如果多次修改响应式数据的值,组件的更新就会频繁发生,这显然是不合理的.我们需要设计一个机制,无论对响应式数据进行多少次修改,副作用函数只会执行一次,为此我们需要实现一个调度器.

//任务缓存队列,可以自动对任务去重
const queue = new Set()
let isFlushing = false
const p = Promise.resolve()

// 调度器主要函数,将任务添加到缓冲队列中,并且开始刷新队列
function queueJob(job) {
    queue.add(job)
    if(!isFlushing) {
        isFlushing = true
        //在微任务中刷新虚拟缓冲队列
        p.then(() => {
            try {
                queue.forEach(job => job())
            } finally {
                isFlushing = false
                queue.length = 0
            }
        })
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

上述是调度器的最小实现,本质上利用了微任务的异步执行机制,实现对副作用的缓冲.

function mountComponent(vnode, container, anchor) {
    const componentOptions = vnode.type
    const { data, render } = componentOptions
    const state = reactive(data())
    effect(() => {
        const subTree = render.call(state,state)
        patch(null, subTree, container, anchor)
    }, {
        //指定调度器
        scheduler: queueJob
    })
}
1
2
3
4
5
6
7
8
9
10
11
12

上述实现仍然存在缺陷.我们在effect函数内调用patch的时候第一个参数总是null,这意味着每次更新都会发生一次全新的挂载.更加合理的做法应该是每次更新的时候进行比较打补丁.

# 组件实例与生命周期

组件实例本质上就是一个状态集合,维护着组件运行的所有信息.为了解决上一节中关于组件更新的问题,我们需要引入组件实例的概念.

function mountComponent(vnode, container, anchor) {
    const componentOptions = vnode.type
    const { data, render } = componentOptions
    
    const state = reactive(data())
    // 创建组件实例
    const instance = {
        //组件自身的状态数据
        state,
        // 一个布尔值,表示组件是否已被挂载
        isMounted: false,
        // 组件所渲染的东西
        subTree: null
    }
    
    vnode.component = instance
    effect(() => {
        const subTree = render.call(state,state)
        if(!instance.isMounted) {
            // 初次挂载
            patch(null, subTree, container, anchor)
            instance.isMounted = true
        } else {
            //完成自更新
            patch(instance.subTree, subTree, container, anchor)
        }
        //更新实例子树
        instance.subTree = subTree
    }, {
        scheduler: queueJob
    })
}
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

在上面的实现中,instance.isMounted可以用来区分组件的挂载与更新,我们可以在合适的时机调用组件对应的生命周期钩子.

function mountComponent(vnode, container, anchor) {
    const componentOptions = vnode.type
    const { data, render, beforeCreate, created, beforeMount, mounted, beforeUpdate, updated } = componentOptions
    beforeCreate && beforeCreate()
    const state = reactive(data())
    const instance = {
        state,
        isMounted: false,
        subTree: null,
    }
    vnode.component = instance
    created && created()
    effect(() => {
        const subTree = render.call(state,state)
        if(!instance.isMounted) {
            beforeMount && beforeMount()
            patch(null, subTree, container, anchor)
            instance.isMounted = true
            mounted && mounted()
        } else {
            beforeUpdate && beforeUpdate()
            patch(instance.subTree, subTree, container, anchor)
            updated && updated()
        }
        instance.subTree = subTree
    }, {
        scheduler: queueJob
    })
    
}
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

上述代码就是组件生命周期的实现原理,实际上可能存在多个同样的组件生命周期钩子,例如来自mixin中的生命周期钩子,因此我们通常需要将组件生命周期序列化为一个数组,但核心原理不变.

写于西13

Last Updated: 2024/9/12 09:02:01