手写Vue源码 watch的实现

watch方法 也叫用户Watcher,是通过new Watcher产生的一个实例。
new Watcher的时候会执行get方法,get方法会将当前用户Wathcer放入Dep.target中,然后执行getter方法,去实例上取值,取值就会调用get方法,进行依赖收集,当值发生变化了就调用watcher的updata方法,然后会再次调用run方法,就会取到新值,如果新值和老值不一样就执行回调函数
function initWatch(vm) { 
    let watch = vm.$options.watch //获取用户传入的watch
    for(let key in watch){ //msg(){}
        let handler = watch[key]
        createWatcher(vm,key,handler)
    }
}
function createWatcher(vm,key,handler){
    return vm.$watch(key,handler)
}


Vue.prototype.$watch = function (expr,handler) {
    //创建一个Watcher expr = msg, handler = ()=>{}
    new Watcher(vm,expr,handler, {user:true}) //{user:true}表示用户自己写的watcher
}



import {pushTarget,popTarget} from './dep'
import { utils } from '../utils'
let id = 0 //watcher的唯一标识
class Watcher{ //每次产生一个watcher都要有一个唯一标识
    constructor(vm,exprOrfn,cb = {},opts = {}){ //cb = {},opts = {} 默认值
        this.vm = vm //当前组件实例
        this.exprOrfn = exprOrfn //判断用户传入的是表达式还是函数msg
        if(typeof exprOrfn === 'function') {
            this.getter = exprOrfn //getter就是传入的第二个参数
        }else { //如果是表达式就将对应得表达式取出来 赋值给getter
            this.getter = function () {
                return utils.getValue(vm,exprOrfn)
            }
        }
        if(opts.user){ //标识用户自己写的Watcher
            this.user = true
        }
        this.cb = cb //用户传入的回调函数 vm.$watch('msg',cb)
        this.opts = {} //其他参数
        this.id = id++
        this.deps = []
        this.depId = new Set()
        //创建watcher得时候先将表达式对应得老值取出来,更新得时候再取新值
        this.value = this.get()
         

        this.get() //每次创建一个watcher就执行自己的get方法
    }
    get() {
        //Dep.target也会存放用户的watcher = user watcher
        pushTarget(this) //渲染watcher保存起来 Dep.target = watcher
        let value = this.getter() //让当前传入的函数updataComponent执行,从实例上取值渲染页面 会调用Object.defineProperty 的get方法
        popTarget() //取完值后 清空当前watcher
        return value //返回新值
    }
    addDep(dep){ //过滤,不要重复存入watcher到dep中,也不要重复存dep。
        let id = dep.id
        if(!this.depId.has(id)) {
            this.depId.add(id)
            this.deps.push(dep) //将dep存入watcher
            dep.addSub(this) //将watcher存入dep
        }
    }
/**
 * 重要部分updata()方法
 * 加入页面有四个地方需要更新属性,那我们希望不要更新四次,而是一次性更新
 * 防止不停的更新
    把需要更新的watcher先存起来 放进一个异步队列queueWatcher,然后通过nextTick异步执行
    把重复的watcher过滤掉
    等待这轮更新完就清空队列 就是说等待主执行栈执行完了就执行异步任务,也可以理解为页面所有属性都赋值完再执行这个异步方法
 */
    updata(){ //如果立即调用get 会导致页面刷新频繁 用异步更新来解决
        // this.get()
        queueWatcher(this) //queueWatcher异步队列
    }
    run(){
        let value = this.get() //新值
        if(this.value !== value) {
            this.cb(value,this.value)
        }
    }
    
}
//渲染watcher、计算属性、vm.$watch 都属于Watcher实例
export default Watcher

function flushQueue () {
    queue.forEach(watcher => watcher.run) //执行更新
    //清空,下次使用
    has = {}
    queue = []
}
let has ={};
let queue = []
function queueWatcher(watcher){
    let id = watcher.id
    if(has[id] == null){
        has[id] =true
        queue.push(watcher) //相同的Watcher只会存一个到queue中

        //延迟清空队列
        // setTimeout(flushQueue,0) 这个方法也可以但vue内部原理是nextTick
        nextTick(flushQueue)
    }
}
let callbacks =[] //有可能用户也会写一个nextTick方法,这时候就需要把nextTick的回调函数放进一个数组里面,再依次执行,
function nextTick(cb){
    callbacks.push(cb)

    //异步刷新这个callbacks 
    //异步任务先执行微任务在执行宏任务,微任务:Promise, mutationObserver, 宏任务:setImmediate setTimeout
    let timeFunc = () => {
        flushCallbacks()
    }
    //判断当前浏览器执行的异步方法
    if(Promise) {
        return Promise.resolve().then(timeFunc)
    }
    if(MutationObserver){ //创建并返回一个新的 MutationObserver 它会在指定的DOM发生变化时被调用
        let observe = new MutationObserver(timeFunc);
        let textNode = document.createTextNode(1)
        observe.observe(textNode,{characterData:true})
        textNode.textContent(2)
        return
    }
    if(setImmediate) {
        return setImmediate(timeFunc)
    }
    //以上方法都不支持的时候就调用setTimeout setTimeout是宏任务,会在下一轮执行 事件循环机制的相关知识,但我们希望尽量再本轮执行,所以先判断支不支持微任务
    setTimeout(timeFunc,0)
}
不积跬步无以至千里
原文地址:https://www.cnblogs.com/lyt0207/p/12520896.html