12.2 组件状态与自更新
组件的状态用
data()声明、用reactive包装;把整个渲染任务包进effect,状态一变渲染函数就重新执行,这就是组件的自更新。
一、为组件设计自身状态
两条约定:
- 用户必须用
data函数返回状态对象,并可在渲染函数里通过this访问它; - 状态的初始化:执行
data()→reactive包装 → 作为render的this与第一个参数。
function mountComponent(vnode, container, anchor) {
const componentOptions = vnode.type
const { render, data } = componentOptions
// 执行 data 得到原始数据,并包装为响应式数据
const state = reactive(data())
// render 的 this 指向 state,同时把 state 作为第一个参数传入
const subTree = render.call(state, state)
patch(null, subTree, container, anchor)
}
用
data()而不是data: {}的原因:同一个组件可能被挂载多次,每次都需要一份全新的状态数据。
二、把渲染任务包进 effect
const state = reactive(data())
effect(() => {
// 执行渲染函数:读取 state → 收集依赖
const subTree = render.call(state, state)
// 渲染
patch(null, subTree, container, anchor)
})
| 环节 | 作用 |
|---|---|
reactive(data()) | 让组件状态变为响应式数据 |
render.call(state, state) | 渲染函数内可用 this.foo,也能通过第一个参数拿状态 |
effect(...) | 把渲染任务变成可被触发的副作用函数 → 具备自更新能力 |
三、用调度器给自更新去重
effect 默认同步执行,一轮里多次修改状态就会渲染多次;而且此时 patch 的第一个参数恒为 null,意味着每次都是全新挂载而不是打补丁。
effect(() => {
const subTree = render.call(state, state)
patch(null, subTree, container, anchor)
}, {
// 指定该副作用函数的调度器
scheduler: queueJob
})
// 任务缓存队列,用 Set 自动去重
const queue = new Set()
// 是否正在刷新队列
let isFlushing = false
// 一个立即 resolve 的 Promise
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.clear()
}
})
}
}
调度器只解决「渲染几次」:入队 +
Set去重 + 微任务统一刷新,一轮多次修改只渲染一次。 「怎么渲染」是另一件事:这一版仍然把null当作旧节点,真正的更新要等 12.3 引入组件实例后才能实现。
四、测试
const MyComponent = {
name: 'MyComponent',
// 用 data 函数定义组件自身的状态
data() {
return { foo: 'hello world' }
},
render() {
setTimeout(() => {
this.foo = 'hello vue'
this.foo = 'hello vue1'
this.foo = 'hello vue2'
}, 2000)
// 在渲染函数内使用组件状态
return { type: 'div', children: `foo 的值是: ${this.foo}` }
}
}
连续三次赋值只触发一次重新渲染(微任务队列去重);去掉
scheduler就会看到渲染函数被同步执行多次。
相关
- 12.1 渲染组件:patch 中的组件分支与 mountComponent 的最小实现
- 12.3 组件实例与组件的生命周期:用组件实例记录上一次的子树,把「重新挂载」换成「打补丁更新」