6. 异步更新的原理

异步更新原理

上一章实现了依赖收集和自动更新, 但是存在问题: 如果对同一个属性多次更新, 如: vm.name = 1 vm.name = 2, vm.name = 3, 就是触发多次update方法, 会导致效率底下.

考虑将需要更新的watcher不是立即执行, 而是维护到一个队列里面去, 同时去重, 带一个更新周期结束后, 依次执行其中的代码, 这样子能有效避免性能的浪费, 就是nextTick的实现原理

watcher.js

import { Dep } from "./dep"


let id = 0

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

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

        // 是否时渲染watcher
        this.renderWatcher = options
        // 重新渲染的方法
        this.getter = fn
        // 渲染watcher需要立即执行一次
        this.get()
    }


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

    // watcher里面添加dep, 去重
    addDep(dep) {
        if(!this.depsId.has(dep.id)) {
            this.deps.push(dep)
            this.depsId.add(dep.id)
            // 去重之后, 让当前的dep,去记住当前的watcher
            dep.addSub(this)
        }
    }

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

    run() {
        this.get()
    }
}


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
    }
}

// nextTick既有同步,也有异步, push的时候是同步的, 执行的时候是异步的,
// 用户的nextTick和内部的nextTick哪个先执行取决于顺序
// nextTick采用的是一种优雅降级的方式实现的

scr/index.js 同时把nextTick挂载到Vue的原型上

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

import { initMixin } from "./init"
import { initLifeCycle } from "./lifecycle"
import { nextTick } from "./observe/watcher"



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

initMixin(Vue)
initLifeCycle(Vue)


Vue.prototype.$nextTick = nextTick

export default Vue
posted @ 2022-06-26 23:19  littlelittleship  阅读(68)  评论(0编辑  收藏  举报