10. watch的实现原理

watch的实现原理

watch和computed一样, 也是基于 Watcher 的

组件内部使用的watch 和 外部使用的 vm.$watch()都是调用的Vue.prototype.$watch方法

当依赖的属性发生变化, 更新的时候执行回调就行了

vue'中watch有多种写法, 这里只简单观察2种

 // watch就是一个观察者, dep发生变化, 就执行对应的回调
            watch: {
                // 字符串形式
                firstname(newValue, oldValue) {
                    console.log(newValue, oldValue)
                }
                // 有多种写法: 
                // 1. 字符串, 内容定义在method里面
                // 2. 函数
                // 3. 数组
            }

vm.$watch(() => vm.firstname, (newValue, oldValue) => {
            console.log(newValue, oldValue, 'ppp');
        })

在Vue.prototype上拓展$watch方法, 里面创建一个用户watcher

// 监控的值, 回调, 选项
Vue.prototype.$watch = function(exprOrFn,cb, options = {}){
    // console.log('333', exprOrFn,cb, options)

    //exprOrFn 可能是fitstname   也可能是  () => vm.firstname
    // cb就是定义的函数
    // 这个watcher功能, exprOrFn变化了, 执行cb
    new Watcher(this, exprOrFn, {user: true}, cb)
}

在state.js种处理 watch 部分

if(opts.data) {
    initData(vm)
}

if(opts.computed) {
    initComputed(vm)
}
if(opts.watch) {
    initWatch(vm)
}

...

// initWatch的实现, 获取所有的watch(数组), 
function initWatch(vm) {
    let watch = vm.$options.watch
    for(let key in watch) {
        const 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) {
    if(typeof handler === 'string') {   // 如: firstname: 'fn'  的形式, 但是fn是定义在methods上面的
        handler = vm[handler]
    }
    return vm.$watch(key, handler)
}

在改造Watcher, 适配用户watcher

// 添加cb属性, 原来的fn,改成exprOrFn, 可能是字符串, 也可能是function'
constructor(vm, exprOrFn, options, cb) {
    // watch的watcher添加了一个cb回调
        this.cb = cb
        this.user = options.user    // 标识是否是用户自己的watcher
    
    // 如果是字符串, 要变成函数
	if(typeof exprOrFn === 'string') {
            this.getter = function() {
                return vm[exprOrFn]             // return vm.firstname
            }
        } else {
            this.getter = exprOrFn
        }
    
   // 注意这个watcher也会立即执行, 要获取返回值, 作为旧的value
      this.value = this.lazy ? undefined : this.get()
    
    
    
    
// 在更新的时候, 获取新旧值, 执行回调
      run() {
        // 获取新旧值
        let oldValue = this.value
        let newValue = this.get()
        if(this.user) {
            this.cb.call(this.vm, newValue, oldValue)
        }

具体 代码

dist/8.watcher.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>计算属性</title>
</head>

<body>
    <div id="app" style="color:yellow;backgroundColor:blue;">
            {{fullname}}  {{fullname}}  {{fullname}}
          
    </div>
    <script src="vue.js"></script>
    <!-- <script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.14/vue.js"></script> -->
    <script>
        const vm = new Vue({
            data() {
                return {
                  firstname: 'yang',
                  lastname: 'jerry'
                }
            },
            el: '#app',  // 将数据解析到el元素上
            // 急速属性, 依赖的指发生变化才会重新执行, 要维护一个dirty属性, 默认计算属性不会立即执行
            // 计算属性就是一个defineProperty  
            // 计算属性也是一个watcher
            computed: {
                // 写法1
                fullname() {
                    return this.firstname + '-' + this.lastname
                }
            },
            // watch就是一个观察者, dep发生变化, 就执行对应的回调
            watch: {
                // 字符串形式
                firstname(newValue, oldValue) {
                    console.log(newValue, oldValue)
                }
                // 有多种写法: 
                // 1. 字符串, 内容定义在method里面
                // 2. 函数
                // 3. 数组
            }
        })
        // 4. $watch  最终都是调用下面这个
        // 这个是函数形式
        vm.$watch(() => vm.firstname, (newValue, oldValue) => {
            console.log(newValue, oldValue, 'ppp');
        })
        
        // 如果有数组嵌套
        setTimeout(() => {
               vm.firstname = '888'
        },1000)
     
    </script>
</body>

</html>

src/index.js

// Vue 类是通过构造函数来实现的
// 如果通过 class来实现, 里面的类和方法就会有很多, 不利于维护
// 1. 新建一个Vue构造函数, 默认导出, 这样就有了全局 Vue 
// 2. Vue中执行一个初始化方法, 参数是用户的选项
// 3. 在Vue的原型上添加这个方法, (注意: 添加的这个方法在引入vue的时候就执行了, 而不是在new Vue()的时候执行的)

import { initGlobalApi } from "./globalApi"
import { initMixin } from "./init"
import { initLifeCycle } from "./lifecycle"
import { nextTick, Watcher } from "./observe/watcher"



function Vue(options) {
    this._init(options)
}

initMixin(Vue)
initLifeCycle(Vue)
initGlobalApi(Vue)


Vue.prototype.$nextTick = nextTick

// 监控的值, 回调, 选项
Vue.prototype.$watch = function(exprOrFn,cb, options = {}){
    // console.log('333', exprOrFn,cb, options)

    //exprOrFn 可能是fitstname   也可能是  () => vm.firstname
    // cb就是定义的函数
    // 这个watcher功能, exprOrFn变化了, 执行cb
    new Watcher(this, exprOrFn, {user: true}, cb)
}



export default Vue

src/state.js

import { observe } from "./observe"
import { Dep } from "./observe/dep"
import { Watcher } from "./observe/watcher"


export function initState(vm) {
    const opts = vm.$options
    if(opts.data) {
        initData(vm)
    }

    if(opts.computed) {
        initComputed(vm)
    }

    if(opts.watch) {
        initWatch(vm)
    }
}


function initWatch(vm) {
    let watch = vm.$options.watch
    for(let key in watch) {
        const 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) {
    if(typeof handler === 'string') {   // 如: firstname: 'fn'  的形式, 但是fn是定义在methods上面的
        handler = vm[handler]
    }
    return vm.$watch(key, handler)
}

// 初始化数据的具体方法
function initData(vm) {
    let data = vm.$options.data
    data = typeof data === 'function' ? data.call(vm) : data

    vm._data = data

    // 进行数据劫持, 关键方法, 放在另一个文件里面, 新建 observe/index.js
    observe(data)

    // 设置代理, 这个代理只有最外面这一层
    // 希望访问 vm.name 而不是 vm._data.name, 使用vm 来代理 vm._data
    // 在vm上取值时, 实际上是在vm._data上取值
    // 设置值时, 实际上是在vm._data上设置值
    // 每一个属性都需要代理
    for(let key in data) {
        proxy(vm, '_data', key)
    }


}

// 属性代理 vm._data.name  => vm.name
function proxy(vm, target, key) {
    Object.defineProperty(vm, key, {
        get() {
            return vm[target][key]
        },
        set(newValue) {
            vm[target][key] = newValue
        }
    })
}

// 初始化计算属性
function initComputed(vm) {
    // 得到的computed时一个数组
    const computed = vm.$options.computed

    for(let key in computed) {
        // 获取computed
        let userDef = computed[key]
        // 定义watcher并挂载到实例上, 方便后续通过实例获取, 把key和watcher一一对应
        const watchers = vm._computedWatchers = {}

        let fn = typeof userDef === 'function' ? userDef : userDef.get

        // 创建一个计算属性watcher
        watchers[key] = new Watcher(vm, fn, {lazy: true})

        // 在vue实例上定义这些属性, 所以可以通过vm.fullname访问到
        defineComputed(vm, key, userDef)

    }
}

function defineComputed(target, key, userDef) {
    const setter = userDef.set || (() => {})
    Object.defineProperty(target, key, {
        get: createComputedGetter(key),
        set: setter
    })
}

function createComputedGetter(key) {
    return function() {
        // 这里的this指向上面的target, 也就是vm
        const watcher = this._computedWatchers[key]
        // 如果是脏值, 求值
        if(watcher.dirty) {
            //  求值之后, dirty变成false, 下次就不求值了     需要在watcher上添加evaluate方法
            watcher.evaluate()
        }
        // 上面取值之后会将计算属性watcher pop出来, 如果stack里面还有watcher, 那就是渲染watcher, 需要计算属性里面的deps去记住上层的watcher
        // 因为计算属性watcher不能更新视图, 只有渲染watcher可以
        if(Dep.target) {
            // 添加depend方法
            watcher.depend()
        }
        // 新增value属性表示计算属性的值
        return watcher.value
    }
}

observe/watcher.js

import { Dep, popTarget, pushTarget } from "./dep"


let id = 0

export class Watcher {
    constructor(vm, exprOrFn, options, cb) {
        this.id = id ++
        this.vm = vm

        this.deps = []
        this.depsId = new Set()

        // 是否时渲染watcher
        this.renderWatcher = options

        this.lazy = options.lazy
        this.dirty = this.lazy

        // watch的watcher添加了一个cb回调
        this.cb = cb
        this.user = options.user    // 标识是否是用户自己的watcher

        // 重新渲染的方法

        // 加入watch的watcher之后, exprOrFn可能不是函数, 是个字符串, 需要变成函数
        if(typeof exprOrFn === 'string') {
            this.getter = function() {
                return vm[exprOrFn]             // return vm.firstname
            }
        } else {
            this.getter = exprOrFn
        }
        // 渲染watcher需要立即执行一次, 计算属性watcher初始化时不执行
        // 用户的watcher也会执行, 获取上一次的旧值
        this.value = this.lazy ? undefined : this.get()
    }


    get() {
        // 开始渲染时, 让静态属性Dep.target指向当前的watcher, 那么在取值的时候, 就能在对应的属性中记住当前的watcher
        // Dep.target = this
        pushTarget(this)
        let value = this.getter.call(this.vm)
        // 渲染完毕之后清空
        // Dep.target = null
        popTarget()
        return value
    }

    // watcher里面添加dep, 去重
    addDep(dep) {
        if(!this.depsId.has(dep.id)) {
            this.deps.push(dep)
            this.depsId.add(dep.id)
            // 去重之后, 让当前的dep,去记住当前的watcher
            dep.addSub(this)
        }
    }
    // 让计算属性watcher里面的dep收集外层的watcher
    depend() {
        let length = this.deps.length
        while(length--) {
            this.deps[length].depend()
        }
    }

    update() {
        if(this.lazy) {
            this.dirty = true
        } else {
            // 更新, 需要重新收集依赖
            queueWatcher(this)      // 把当前的watcher暂存起来
        }
    }

    run() {
        // 获取新旧值
        let oldValue = this.value
        let newValue = this.get()
        if(this.user) {
            this.cb.call(this.vm, newValue, oldValue)
        }

    }

    evaluate() {
        this.value = this.get()
        this.dirty = false
    }
}


let queue = []  // 用于存放需要更新吧的watcher
let has = {}    // 用于去重
let pending = false     // 防抖

function flushScheduleQueue() {
    let flushQueue = queue.slice(0)     // copy一份
   
    queue = []      // 刷新过程中, 有新的watcher, 重新放到queue中
    has = {}
    pending = false
    flushQueue.forEach(q => q.run())    // 添加一个run方法,真正的渲染
}

function queueWatcher(watcher) {
    const id = watcher.id
    if(!has[id]) {      // 对watch进行去重
        queue.push(watcher)
        has[id] = true
        // 不管update执行多少次, 但是最终值执行一次刷新操作

        if(!pending) {
            // 开一个定时器     里面的方法只执行一次, 并且是在所有的watcher都push进去之后才执行的
            // setTimeout(() => {
            //     console.log('杀心')
            // }, 0)
            // setTimeout(flushScheduleQueue, 0)

            nextTick(flushScheduleQueue, 0)     // 内部使用的是nextTick, 第二个参数估计可以不要

            pending = true
        }
    }
}

let callbacks = []
let waiting = false
// 跟上面的套路一样
function flushCallBacks() {
    let cbs = callbacks.slice(0)
    waiting = false
    callbacks = []
    cbs.forEach(cb => cb())
}

// vue内部 没有直接使用某个api, 而是采用优雅降级的方式
// 内部先使用的是promise(ie不兼容),   MutationObserver (h5的api)  ie专享的 setImmediate  最后setTimeout

let timerFunc
if(Promise) {
    timerFunc = () => {
        Promise.resolve().then(flushCallBacks)
    }
} else if(MutationObserver) {
    let observer = new MutationObserver(flushCallBacks)     // 这里传入的回调时异步执行的
    let textNode = document.createTextNode(1)   // 应该是固定用法
    observer.observe(textNode, {
        characterData: true
    })
    timerFunc = () => {
        textNode.textContent = 2
    }
} else if(setImmediate) {
    timerFunc = () => {
        setImmediate(flushCallBacks)
    }
} else {
    timerFunc = () => {
        setTimeout(flushCallBacks)
    }
}

export function nextTick(cb) {         // setTimeout是过一段事件后, 执行cb, nextTick是维护了一个队列, 后面统一执行
    callbacks.push(cb)
    if(!waiting) {
        // setTimeout(() => {
        //     flushCallBacks()
        // }, 0)
        timerFunc()
        waiting = true
    }
}

整个流程: 在初始化状态的时候, 如果有watche(数组),遍历watche,并为每一项生成一个用户watcher, 默认这个watcher会立即执行并取值, 记录在value上, 作为oldValue, 当依赖的属性发生变化, 会重新取值, 此时的值就是newValue, 然后判断是否是用户的watcher, 如果是, 执行传入的回调

posted @ 2022-06-27 13:07  littlelittleship  阅读(301)  评论(0编辑  收藏  举报