My Little World

Mobx 运行机制深入研究

追踪原理

官方文档

MobX 会对在执行 跟踪函数 期间 读取的任何现有的可观察属性做出反应

读取” 是对象属性的间接引用,可以用过 . (例如 user.name) 或者 [] (例如 user[‘name’]) 的形式完成。

追踪函数” 是 computed 表达式、observer 组件的 render() 方法和 when、reaction 和 autorun 的第一个入参函数。

过程(during)” 意味着只追踪那些在函数执行时被读取的 observable 。这些值是否由追踪函数直接或间接使用并不重要。

换句话说,MobX 不会对其作出反应:

从 observable 获取的值,但是在追踪函数之外

在异步调用的代码块中读取的 observable

Mobx 5 以下 MobX 不会追踪还不存在的索引或者对象属性(当使用 observable 映射(map)时除外)。

所以建议总是使用 .length 来检查保护基于数组索引的访问。

所有数组的索引分配都可以检测到,但前提条件必须是提供的索引小于数组长度。

核心概念

追踪属性访问,而不是值

1
2
3
4
5
6
7
8
9
let message = observable({
title: "Foo",
author: {
name: "Michel"
},
likes: [
"John", "Sara"
]
})

mobx会追踪箭头有没有变化

如果箭头发生变化,就会执行追踪函数

使用注意

处理数据时

1.更改没有被obserable的箭头,追踪函数不执行

2.追踪函数里使用间接引用指向obserable属性,追踪函数不执行

3.对新增的属性,可以使用set,get实现obserable

4.在异步代码中访问的obserable属性,不会引起追踪函数执行

1.更改没有被obserable的箭头,追踪函数不执行
autorun(() => {
    console.log(message.title)
})
message = observable({ title: "Bar" }) //指向message的箭头没有被obervable
autorun(() => {
    message.likes;//箭头没变,又没有访问数组里面的属性
})
message.likes.push("Jennifer");

2.追踪函数里使用间接引用指向obserable属性,追踪函数不执行
var title = message.title;
autorun(() => {
    console.log(title) //访问箭头没有变,还是指向老值的位置
})
message.title = "Bar" //箭头改了,但autorun里没有用到
const author = message.author;
autorun(() => {
    console.log(author.name) 
})
message.author.name = "Sara";//会执行跟踪函数,autorun里有访问name属性,这里指向name值得箭头改了
message.author = { name: "John" };//不会执行,没有访问author属性的箭头

正确使用

A:
autorun(() => {
    console.log(message.author.name)
})
message.author.name = "Sara";
message.author = { name: "John" };
B:
function upperCaseAuthorName(author) {
    const baseName = author.name;
    return baseName.toUpperCase();
}
autorun(() => {
    console.log(upperCaseAuthorName(message.author))
})
message.author.name = "Chesterton"

3.异步
const message = observable({ title: "hello" })
autorun(() => {
    console.log(message) //会执行两次,因为console.log是异步的,请确保始终传递不变数据 ( immutable data ) 或防御副本给 console.log。
})
message.title = "Hello world"
autorun(() => {
    setTimeout(
        () => console.log(message.likes.join(", ")), //异步执行,访问原始数据打印一次
        10
    )
})
message.likes.push("Jennifer");//不会引起autorun执行

4.MobX 5 可以追踪还不存在的属性
autorun(() => {
    console.log(message.postDate)
})
message.postDate = new Date()

组件使用时

子组件问题

MobX 只会为数据是直接通过 render 存取的 observer 组件进行数据追踪

所以当需要将数据传递给子组件时,要保证子组件也是一个obserable组件,可以做出反应

解决办法:

1.将子组件使用obserable函数处理

它用 mobx.autorun 包装了组件的 render 函数以确保任何组件渲染中使用的数据变化时都可以强制刷新组件

2.使用mobx-react的Obserable组件包裹子组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
方法一:将子组件使用obserable函数处理
const MyComponent = observer(({ message }) =>
<SomeContainer
title = {() => <TitleRenderer message={message} />}
/>
)
const TitleRenderer = observer(({ message }) =>
<div>{message.title}</div>}
)
message.title = "Bar"
方法二:使用mobx-react的Obserable组件包裹子组件
const MyComponent = ({ message }) =>
<SomeContainer
title = {() =>
<Observer>
{() => <div>{message.title}</div>}
</Observer>
}
/>
message.title = "Bar"

避免在本地字段中缓存 observable

1
2
3
4
5
6
7
8
9
10
@observer class MyComponent extends React.component {
author;
constructor(props) {
super(props)
this.author = props.message.author;//message.author发生变化时不会引起render
}
render() {
return <div>{this.author.name}</div> //.name可以引起render
}
}

优化,使用计算属性,或者在render函数中进行间接引用

@observer class MyComponent extends React.component {
    @computed get author() {
        return this.props.message.author
    }

其他

1.从性能上考虑,越晚进行间接引用越好

2.数组里面的是对象而不是字符串,那么对于发生在某个具体的对象中发生的变化,渲染数组的父组件将不会重新渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const Message = observer(({ message }) =>
<div>
{message.title}
<Author author={ message.author } />
<Likes likes={ message.likes } />
</div>
)
const Author = observer(({ author }) =>
<span>{author.name}</span>
)
const Likes = observer(({ likes }) =>
<ul>
{likes.map(like =>
<li>{like}</li>
)}
</ul>
)
变化 重新渲染组件
message.title = “Bar” Message
message.author.name = “Susan” Author (.author 在 Message 中进行间接引用, 但没有改变)*
message.author = { name: “Susan”} Message, Author
message.likes[0] = “Michel” Likes

一些 对比

autorun vs compute

当使用 autorun 时,所提供的函数总是立即被触发一次,然后每次它的依赖关系改变时会再次被触发

computed(function) 创建的函数只有当它有自己的观察者时才会重新计算,否则它的值会被认为是不相关的

如果一个计算值不再被观察了,例如使用它的UI不复存在了,MobX 可以自动地将其垃圾回收。

而 autorun 中的值必须要手动清理才行

autorun vs reaction

reaction(() => data, (data, reaction) => { sideEffect }, options?)

它接收两个函数参数,第一个(数据函数)是用来追踪并返回数据作为第二个函数(效果函数)的输入。

传入 reaction 的第二个函数(副作用函数)当调用时会接收两个参数。

第一个参数是由 data 函数返回的值。

第二个参数是当前的 reaction,可以用来在执行期间清理 reaction

reaction 返回一个清理函数。

不同于 autorun 的是当创建时 **效果 **函数不会直接运行,只有在数据表达式首次返回一个新值后才会运行。

在执行 效果函数时访问的任何 observable 都不会被追踪。

效果函数仅对数据函数中访问的数据作出反应,这可能会比实际在效果函数使用的数据要少。

此外,效果 函数只会在表达式返回的数据发生更改时触发。 换句话说: reaction需要你生产 效果函数中

所需要的东西。

useObserver vs Observer vs observer

相关文档

1.虽然只是在返回DOM的地方使用 useObserver(), 但是,当dom中数据改变的时候,整个component都会重新render

1
2
3
4
5
6
7
8
9
10
function Person() {
console.log('in useObserver');//点击按钮会触发执行
const person = useLocalStore(() => ({ name: 'John' }));
return useObserver(() => (
<div>
{person.name}
<button onClick={() => (person.name = 'Mike')}>No! I am Mike</button>
</div>
));
}

2.Observer 标签组件可以更精准的控制想要重新渲染的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default function ObservePerson() {
console.log('in Observer');//点击按钮不会执行
const person = useLocalStore(() => ({name: 'John'}))
return (
<div>
The old name is: {person.name} //点击按钮不会更新
<div>
<Observer>{() => <div>{person.name}</div>}</Observer> //点击按钮会更新
<button onClick={() => (person.name = 'Mike')}>
I want to be Mike
</button>
</div>
</div>
)
}

3.与useObserver相比,除了使用方法不同,目前不知道区别在哪,有时间需要探究一下

const ObserverLowercasePerson: React.FC<any> = observer(() => {
    console.log('in Observer') //点击按钮也会执行
    const person = useLocalStore(() => ({name: 'John'}));
    return (
        <div>
            <div>The name is: {person.name}</div>
            <button onClick={() => (person.name = 'Mike')}>
                Change name
            </button>
        </div>
    )
})
```