likes
comments
collection
share

vue2源码解析(四):依赖收集

作者站长头像
站长
· 阅读数 9

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情

你好呀,我是小九,很高兴见到你。

摘要

vue2源码学习之路。

查看之前的文章

vue2源码解析(一):环境搭建

vue2源码解析(二):数据劫持

vue2源码解析(三):从html模板到真实dom呈现全过程分析

在上一篇文章中,实现了从html模板到真实dom渲染的过程,简单回顾一下。

首先进行模板编译,将模板编译成ast语法树,然后将ast生成render函数。

调用render函数生成虚拟dom,最后再将虚拟dom转换成真实的dom。

整个过程中,有两个关键的函数,一是_render函数用于创建虚拟dom,_update函数负责将虚拟dom转成真实dom。

如果数据发生变化,视图也要随之变化,就要重新进行渲染。

此时就需要重新调用_render_update函数。

通过手动调用_render函数和_update函数能够实现更新视图的效果

<div id="app" class="test" style="color: red">
    <div class="number">{{a}}</div>
    <div>name: {{obj.name}}</div>
    <div>age: {{obj.age}}</div>
</div>

const app = new Vue({
    el: "#app",
    data: {
        a: 100,
        obj: {
            name: "tom",
            age: 10,
        },
    },
});

setTimeout(() => {
    app.a = 20;
    app.obj.name = "小张";
    app.obj.age =12;
    app._update(app._render())
}, 1000);

结果

vue2源码解析(四):依赖收集

但实际上,不可能每次更新都手动调用_render_update函数。

此时就需要实现数据的依赖收集,数据变化自动更新视图。

接下来就介绍一下如何实现自动更新。

项目结构

vue2源码解析(四):依赖收集

vue更新策略

先来了解一下vue的更新策略。

vue更新策略是以组件为单位的,每个组件都有一个watcher,这个watcher也叫做渲染watcher。

组件中的属性变化后,就会调用当前组件对应的watcher,重新进行渲染。

watcher的本质就是将渲染的逻辑封装起来,也就是_render_update函数。

这里顺便说一句,为什么vue要组件化,面试中可能会问到。

为什么要组件化,组件化的好处是什么?

一是复用,二是方便维护,三是局部更新

封装Watcher

1.封装_render_update

在之前的代码中,_render_update在挂载函数mountComponent中调用。

为了能够自动更新数据,这里,使用Watcher对这两个函数进行包装。

export function mountComponent(vm, el) {
    const updateComponent = () => {
        vm._update(vm._render());
    };
    new Watcher(vm, updateComponent, () => {}, {}, true);
}

此时就需要一个Watcher类。

创建文件,路径src-->observer-->watcher.js,创建一个Watcher类。

2. 基本参数

一个页面可能有很多组件,每一个组件都有一个watcher,因此每个watcher都需要添加一个唯一标识。

Watcher接收几个参数

vue2源码解析(四):依赖收集

vm:当前vue实例

expOrFn:表达式或函数

cb:回调函数,用于更新的函数

options:选项

isRenderWatcher:是不是渲染watcher,只是起了一个名字。

如果expOrFn是一个函数,就把这个函数放到getter上。

创建一个get函数,get函数用来执行getter。

get会默认执行一次,让页面渲染。

import { observer } from ".";
import { isFunction } from "../shared/until";

// 每个watcher都添加一个唯一值
let uid = 0;

class Watcher {
    constructor(vm, expOrFn, cb, options, isRenderWatcher) {
        this.id = ++uid; 
        this.vm = vm;
        this.cb = cb;
        if (isFunction(expOrFn)) {
            this.getter = expOrFn;
        }      
        
        this.get();
    }
    get() {
        this.getter();
    }
}

export default Watcher;

封装Dep

一个页面有可能有多个组件,每个组件都有自己的数据,每个组件对应一个watcher,因此要将组件的数据和watcher绑定在一起。

有些数据在页面中使用了,有些数据没有使用,所以需要知道模板中使用了哪些属性。

可以给模板中使用的每一个属性,增加一个收集器dep,目的就是收集watcher。

1. dep和watcher的对应关系

一个组件对应一个watcher,一个组件中有多个属性,一个属性对应一个dep,所以可能存在多个dep对应一个watcher。

一个属性可以在多个组件中使用,一个属性对应多个组件,所以可能存在一个dep对应多个watcher。

dep和watcher之间是多对多的关系。

vue2源码解析(四):依赖收集

2. 创建Dep类

创建文件,路径src-->observer-->dep.js

dep可能有很多个,所以和watcher一样,需要一个唯一标识。

let uid = 0;

class Dep {
    constructor() {
        this.id = uid++;
    }
}
Dep.target = null;

export default Dep;

vue源码中借鉴了js的单线程模型。

在Dep类上有一个target属性,用于保存当前渲染的watcher,渲染哪个watcher,就保存哪个,渲染完成之后,就把当前的target清空。

注意,target不需要定义在Dep原型上,不是给实例用的,只是相当于定义了一个全局变量。

3.给属性添加dep

在之前的代码中,有一个defineReactive函数,这个函数用来实现数据响应式,也就是数据劫持。

本质上是使用Object.defineProperty给每一个属性添加get和set方法。

获取数据就调用get方法,修改数据就调用set方法。

因此,可以在这个函数中给每个属性添加一个收集器dep,用于收集watcher。

使用get方法取值时,进行依赖收集,使用set方法修改时,进行依赖更新。

简单来说,当页面取值时,说明这个值用来渲染了,此时可以把watcher和属性对应起来。

当页面更新时,就调用watcher封装起来的_render_update函数,重新渲染。

export function defineReactive(data, key, value) {
    observer(value);

    // 给每个属性都添加一个dep
    let dep = new Dep();

    Object.defineProperty(data, key, {
        // 取值时依赖收集
        get() {
            if (Dep.target) {
                dep.depend();
            }
            return value;
        },
        // 修改时依赖更新
        set(newValue) {
            if (newValue === value) return;
            observer(newValue);
            value = newValue;
           
            dep.notify();
        },
    });
}

4. watcher和dep建立双向记忆

上面的代码,如果Dep.target变量中,说明当前有一个渲染的watcher,那么就让dep记住当前的watcher。

if (Dep.target) {
    dep.depend();
}

调用set后,数据更新,页面重新渲染。

dep.notify();

因此,需要在合适的时候,将当前的wathcer保存到全局变量Dep.target上。

前面封装watcher类时,watcher中有一个get方法,本质是调用渲染函数。

所以,需要在渲染之前,将watcher保存在全局变量Dep.target上,渲染完成后将这个变量清空。

class Watcher {
    ...
    get() {
        // 将当前的watcher放到全局变量上
        // this是当前watcher实例
        pushTarget(this);

        // 首先调用一次,让页面渲染
        this.getter();

        // 视图渲染完成后,清空这个值
        popTarget();
    }
    ...
}

在src-->observer-->dep.js 中添加pushTarget函数和popTarget函数

export function pushTarget(watcher) {
    Dep.target = watcher;
}

export function popTarget() {
    Dep.target = null;
}

难点来了,如何让dep和watcher记住对方?

源码中的思路是,在Dep和Watcher类中分别创建一个保存的函数,调用对方的函数,同时将自己的实例传进去,并借助Dep.target这个中间变量。

如图所示

vue2源码解析(四):依赖收集

Dep类中添加addSub方法用于添加watcher

addSub(sub) {
    this.subs.push(sub);
}

添加depend方法让watcher也记住dep。

depend() {
    Dep.target.addDep(this); 
}

Watcher类中添加addDep方法,记住dep。

这里有一点需要注意,如果页面中对于一个属性,使用了多次,每次使用,都让当前的watcher保存一次这个属性的dep,就会出现重复。

vue2源码解析(四):依赖收集

因此,就需要对dep去重。

源码中使用的是Set,先判断dep是否存在,再决定是否保存dep。

constructor{
    this.deps = [];
    this.depIds = new Set();
}

addDep(dep) {
    const id = dep.id;
    if (!this.depIds.has(id)) {
        this.deps.push(dep);
        this.depIds.add(id);
        dep.addSub(this);
    }
}

至此,watcher和dep建立双向记忆完成。

5, 重新渲染

defineReactive这个函数中,get时调用了dep.depend()记住watcher,set时调用了dep.notify()重新渲染。

dep.notify()的逻辑就是遍历subs中保存的所有watcher,调用watcher中的渲染方法。

在Dep类中添加notify函数

notify() {
    this.subs.forEach((watcher) => watcher.update());
}

Watcher类中添加update函数,调用自身的get方法,重新渲染。

update() {
    this.get();
}

测试

还是刚才的代码,这次可以实现自动更新

<div id="app" class="test" style="color: red">
<div class="number">{{a}}</div>
<div>name: {{obj.name}}</div>
<div>age: {{obj.age}}</div>
</div>

<script>
const app = new Vue({
    el: "#app",
    data: {
        a: 100,
        obj: {
            name: "小明",
            age: 10,
        },
    },
});

setTimeout(() => {
    app.a = 20;
    app.obj.name = "自动更新的name";
    app.obj.age = "自动更新的age";
}, 1000);

结果

vue2源码解析(四):依赖收集

总结

每一个属性都有一个dep,每一个组件都有一个watcher,watcher和dep是多对多的关系。

当创建渲染watcher的时候,会把当前的渲染watcher放到Dep的target上,调用_render方法取值,就会执行到属性的get方法,将当前渲染的watcher保存到全局属性上,然后让dep保存watcher,同时也将dep保存到这个watcher上。

每个属性有一个dep,属性就是被观察者,watcher就是观察者,属性变化了会通知观察者更新,这就是观察者模式。

watcher和dep相互记忆的过程很绕,需要好好理解,希望我们一起加油。

完整代码,请移步gihub vue2-source


文章就到这里,下次再见!