vue源码学习14:watch的实现原理

427

前言

面试的时候,总是逃不开一个问题,那就是Vue中的watch是如何实现的?

在我学习Vue源码到第14节的时候,总算要面对这个问题了。我此刻也知道了,这个问题还要从响应式原理和依赖收集说起

我在vue源码学习11:响应式原理和依赖收集一文中,深入的学习了vue的响应式原理和依赖收集,在这篇文章中获悉:

每一个组件都有一个watcher,每一个watcher内部都有一个deps,保存着这个watcher观测的所有数据。

当用户写一个watch的时候,也创建了一个watcher,并且在vue的内部,同时给这个watcher添加了user变量,标注这个watcher是用户自己创建的,和页面渲染创建的watcher做出了区别。

从watch用法说起

在vue中,watch通常有这几种用法:

let vm = new Vue({
    el: '#app',
    data() {
        return { name: '张三', score: { en: 18 } }
    },
    watch: {
        // 用法1:
        name(newVal, oldVal) {
            console.log('newVal', newVal)
            console.log('oldVal', oldVal)
        },
        // 用法2:
        name: [
            function (newVal, oldVal) {
                console.log('newVal', newVal)
                console.log('oldVal', oldVal)
            },
            function (newVal, oldVal) {
                console.log('newVal', newVal)
                console.log('oldVal', oldVal)
            }
        ],
        // 用法3:
        'score.en'(newVal, oldVal) {
            console.log('newVal', newVal)
            console.log('oldVal', oldVal)
        }
    }
});
setTimeout(() => {
    vm.name = '李四';
}, 1000);

这里只介绍这几种基础用法的实现核心。

分别是:

  • 普通的用法
  • 数组里面多个函数的用法
  • 'xxxx.yy'的对象的属性的监听方法的实现

数据劫持时对watch做了什么?

在Vue中,数据劫持的时候,对Vue进行数据初始化,如果发现劫持的数据中,有一个属性名称是watch,将会进行initWatch处理

/**
 * 
 * @param {Vue的实例} vm 
 * initState 说明:
 * 对Vue的数据进行初始化
 * Vue的数据来源有:data,computed,watch,props,inject...
 */
export function initState(vm) {
    // ...省略其他代码
    if (opts.watch) {
        // 对数据进行处理
        initWatch(vm, opts.watch)
    }
}

initWatch

initWatch接受两个参数,分别是vmwatch

watch是一个对象,此时需要对这个对象进行遍历,取出每一个watch对应的操作handler

即:let handler = watch[key]

在前文的用法中,有一种watch的用法,可能是一个数组,所以要对watch进行判断,如果是数组和不是数组分别进行处理,代码如下

function initWatch(vm, watch) {
    for (let key in watch) {
        let handler = watch[key]
        if (Array.isArray(handler)) {
            for (let i = 0; i < handler.length; i++) {
                createWatcher(vm, key, handler[i])
            }
        } else {
            createWatcher(vm, key, handler)
        }
    }
}

function createWatcher(vm, key, handler) {
    // new Watcher()
    return vm.$watch(key, handler)
}

createWatcher接受三个参数,分别是vm、key、handler

  • vm: vue的实例
  • key: 每一个watch的属性名称
  • handler: 每一个watch对应的操作

通过vm.$watch(key, handler)这个方法就很容易看出,在vue的原型上,挂载了一个$watch的方法。

所以在Vue中会有这样一种写法:

vm.$watch('name', function(newVal, oldVal) { // todo some thing })

在Vue实例上挂载watch方法

Vue在初始化的时候,会在原型上挂载一个$watch方法。这个方法就是平时用的watch方法。在这个方法中,它标识了是用户创建的watcher,并且实例化了一个watcher。

这个watcher和组件的wathcer并没有什么太多的区别,区别在于这是用户自己创建watch的时候生成的。

export function stateMixin(Vue) {
    Vue.prototype.$watch = function (key, handler, options = {}) {
        // 用户自己写的watcher和渲染watcher区分
        options.user = true
        new Watcher(this, key, handler, options)
    }
}

回顾watcher

在之前的文章中,watcher类初始化接受4个参数:

class Watcher {
    constructor(vm, exprOrFn, cb, options) {}
}

他们分别是:

  • vm: vue实例
  • exprOrFn: 页面重新渲染的方法 cb和option在上一节中并没有用到。

到了这里,如果是用户传入的watcher就会有所不同了。

  1. 首先需要对exprOrFn参数进行判断,如果是函数,则继续原来的渲染页面的操作,如果是字符串,则需要重新处理
  2. 获取第一次渲染的value进行保存,作为watch中的oldValue
  3. 每次数据发生变化的时候,将会获取最新的值,把新的值赋值给value,作为下一次执行的时候的旧值
  4. 执行cb回调

具体代码如下:

// 一个组件对应一个watcher
let id = 0;
import { popTarget, pushTarget } from './dep';
import { queueWatcher } from './scheduler';
class Watcher {
    constructor(vm, exprOrFn, cb, options) {
        this.vm = vm
        this.exprOrFn = exprOrFn
        this.user = !!options.user // 标识是不是用户写的watcher
        this.cb = cb
        this.options = options
        this.id = id++ // 给watcher添加标识
        // 默认应该执行exprOrFn
        // exprOrFn 做了渲染和更新
        // 方法被调用的时候,会取值
        if (typeof exprOrFn == 'string') {
            // 这里需要将表达式转换成函数
            this.getter = function () {
                // 当数据取值的时候,会进行依赖收集
                // 每次取值的时候,用户自己写的watcher就会被收集
                // 这里的取值可以类比页面渲染的取值{{}}
                let path = exprOrFn.split('.')
                let obj = vm
                for (let i = 0; i < path.length; i++) {
                    obj = obj[path[i]]
                }
                return obj // 走getter方法
            }
        } else {
            this.getter = exprOrFn
        }
        this.deps = []
        this.depsId = new Set()
        // 默认初始化执行get
        // 第一次渲染的时候的value
        this.value = this.get()
        console.log('value', this.value, this)
    }
    get() {
        // 每次获取的时候,会把当前的watcher存放到dep队列中
        pushTarget(this)
        // 这里拿到的值是每一次新的值
        const value = this.getter()
        popTarget() // 这里去除Dep.target,是防止用户在js中取值产生依赖收集
        return value
    }
    update() {
        queueWatcher(this)
    }

    run() {
        let newValue = this.get()
        let oldValue = this.value
        this.value = newValue // 为了保证下一次更新的时候,这一个新值是下一个的老值
        if (this.user) {
            console.log('this.cb', this.cb)
            this.cb.call(this.vm, newValue, oldValue)
        }
    }
    addDep(dep) {
        let id = dep.id
        if (!this.depsId.has(id)) {
            this.depsId.add(id)
            this.deps.push(dep)
            dep.addSub(this)
        }
    }

}
export default Watcher

总结

最后,自己做一点小小的总结:

  1. 用户写watch的时候,会创建一个watcher
  2. 给这个watcher会添加一个用户创建的标识,用来调用传入的回调
  3. watcher会不停的用新值提换老的值,这样在watch回调的时候,可以获取到一个新的值,一个旧的值
  4. 每当这个数据被修改的时候,会触发setter,去执行dep.notify(),通知dep中的watcher全部执行update方法,update方法会经过一个nextTick的队列,去执行run方法,在run方法中,发现这个watcher是用户自己定义的,则执行watch的回调,获取新值和旧值,执行用户定义的事件。

这里的逻辑有点绕,感觉自己的文章表述不是很清晰,望各位看官见谅。