美文网首页
[vue源码06] - Vue.nextTick 和 VM.$n

[vue源码06] - Vue.nextTick 和 VM.$n

作者: woow_wu7 | 来源:发表于2021-09-27 08:50 被阅读0次
    image

    导航

    [深入01] 执行上下文
    [深入02] 原型链
    [深入03] 继承
    [深入04] 事件循环
    [深入05] 柯里化 偏函数 函数记忆
    [深入06] 隐式转换 和 运算符
    [深入07] 浏览器缓存机制(http缓存机制)
    [深入08] 前端安全
    [深入09] 深浅拷贝
    [深入10] Debounce Throttle
    [深入11] 前端路由
    [深入12] 前端模块化
    [深入13] 观察者模式 发布订阅模式 双向数据绑定
    [深入14] canvas
    [深入15] webSocket
    [深入16] webpack
    [深入17] http 和 https
    [深入18] CSS-interview
    [深入19] 手写Promise
    [深入20] 手写函数

    [react] Hooks

    [部署01] Nginx
    [部署02] Docker 部署vue项目
    [部署03] gitlab-CI

    [源码-webpack01-前置知识] AST抽象语法树
    [源码-webpack02-前置知识] Tapable
    [源码-webpack03] 手写webpack - compiler简单编译流程
    [源码] Redux React-Redux01
    [源码] axios
    [源码] vuex
    [源码-vue01] data响应式 和 初始化渲染
    [源码-vue02] computed 响应式 - 初始化,访问,更新过程
    [源码-vue03] watch 侦听属性 - 初始化和更新
    [源码-vue04] Vue.set 和 vm.$set
    [源码-vue05] Vue.extend

    [源码-vue06] Vue.nextTick 和 vm.$nextTick

    前置知识

    (1) 一些单词

    (2) Vue.nextTick() - api使用

    • Vue.nextTick([callback, context] )
      • 参数
        • callback 一个函数
        • context 一个对象
      • 返回值
        • <font color=red>如果没有传入第一个参数函数,并且环境支持promise,则返回一个promise实例对象</font>
        • Vue.nextTick().then() - 因为返回promise所以可以继续调用 .then() 方法
      • 作用
        • 在下次DOM更新循环结束后执行延时回调,在修改数据后立即执行该方法,获取更新后的DOM
        • 解析:<font color=red>当数据修改后,DOM不是立即更新的,而是在下一个 TICK 中去更新,即利用异步任务队列的执行时机去实现,根据具体的环境使用微任务或者红任务队列去更新DOM </font>
      • 需求
        • 要在数据更后,立即获取DOM,而此时DOM并没有更新,需要拿到最新的DOM怎么办?
        • 就可以使用 Vue.nextTick() 或者 VM.$nextTick() 在更新数据后获取更新后的DOM
      • 官网链接 https://cn.vuejs.org/v2/api/#Vue-nextTick
      • 实例
    // 修改数据
    vm.msg = 'Hello'
    // DOM 还没有更新
    Vue.nextTick(function () {  // ----------------------------------------------- 使用方法1 - callback
      // DOM 更新了,可以获取更新后的dom
      // 比如:
        // console.log(this.$refs.messageDom.innerHTML, "DOM - 用了nextTick");
    })
    
    // 作为一个 Promise 使用 (2.1.0 起新增,详见接下来的提示)
    Vue.nextTick()
      .then(function () { // ----------------------------------------------------- 使用方法2 - promise
        // DOM 更新了
      })
      
    async updateMessage2() { // -------------------------------------------------- 使用方法3 - async await
      this.message = "更新了";
      console.log(this.$refs.messageDom.innerHTML, "DOM - 未用nextTick"); // 未更新
      await this.$nextTick();
      console.log(this.$refs.messageDom.innerHTML, "DOM - 用了nextTick"); // 更新了
    }
    
    
    
    
    
    ---- 完整代码
    <template>
      <div class="learn-source-code-vue-nextTick">
        learn-source-code-vue-nextTick 组件
        <div ref="messageDom">message: {{this.message}}</div>
        <button @click="updateMessage">更新message</button>
      </div>
    </template>
    
    <script>
    export default {
      name: "LearnSourceCodeVueNextTick",
      data() {
        return {
          message: "未更新"
        };
      },
      methods: {
        updateMessage() {
          // this.message = "更新了";
          // console.log(this.$refs.messageDom.innerHTML, "DOM - 未用nextTick");
    
          // 写法一
          // this.$nextTick(() => {
          //   console.log(this.$refs.messageDom.innerHTML, "DOM - 用了nextTick");
          // });
    
          // 写法二
          // this.$nextTick().then(() => {
          //   console.log(this.$refs.messageDom.innerHTML, "DOM - 用了nextTick");
          // });
          
          this.updateMessage2();
        },
        async updateMessage2() {
          this.message = "更新了";
          console.log(this.$refs.messageDom.innerHTML, "DOM - 未用nextTick");
          await this.$nextTick();
          console.log(this.$refs.messageDom.innerHTML, "DOM - 用了nextTick");
        }
      }
    };
    </script>
    

    (3) 宏任务和微任务

    • 微任务

      • promise.then
      • MutationObserver
      • process.nextTick - (Node.js 环境)
    • 宏任务

      • setTimeout
      • setInterval
      • setImmediate - (Node.js 环境)

    (4) MutationObserver

    • 作用
      • 监视DOM的变化,DOM的任何变动,该api都会得到通知
      • 可以理解为 DOM 发生变动就会触发 Mutation Observer 事件
      • 比如:节点的增减,属性的变动,文本内容的变动通过MutationObserver都会得到通知
    • 特点
      • 它是 ( 异步触发 ),DOM 的变动并不会马上触发,而是要等到当前所有 DOM 操作都结束才触发
      • 它等待所有脚本任务完成后,才会运行 ( 即异步触发方式 )
      • 它把 DOM 变动记录封装成一个 ( 数组 ) 进行处理,而不是一条条个别处理 DOM 变动
      • 它既可以观察 DOM 的所有类型变动,也可以指定只观察某一类变动
    • 使用实例
      • <font color=red>const obsersver = new MutationObserver(cb) </font>
        • 生成 obsersver 实例
        • 当观察的DOM变化时,触发cb回调
      • <font color=red>obsersver.observe(rootDom, {...}) </font>
        • 观察rootDom节点的dom变化
        • 第二个参数是 配置对象
          • <font color=red>childList</font>:子节点的变动(指新增,删除或者更改)
          • <font color=red>attributes</font>:属性的变动
          • <font color=red>characterData</font>:节点内容或节点文本的变动
          • <font color=red>subtree</font>:布尔值,表示是否将该观察器应用于该节点的所有后代节点
          • <font color=red>attributeOldValue</font>:布尔值,表示观察attributes变动时,是否需要记录变动前的属性值
          • <font color=red>characterDataOldValue</font>:布尔值,表示观察characterData变动时,是否需要记录变动前的值
          • <font color=red>attributeFilter</font>:数组,表示需要观察的特定属性(比如['class','src'])
    <!DOCTYPE html>
    <html lang="en">
    
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
    </head>
    
    <body>
      <div id="root">root</div>
      <div id="root-sibling">sibling-root</div>
    
      <div id="button-dom">点击改变root-DOM</div>
      <div id="button-sibling-dom">点击改变siblingRoot-DOM</div>
      <script>
        const rootDom = document.getElementById('root')
        const siblingRootDom = document.getElementById('root-sibling')
    
        const cb = (mutations, observer) => {
          console.log(mutations, 'mutations')
          console.log(observer, 'observer')
          console.log('new MutationObserver(cb)的cb回调触发')
          console.log('DOM变动了') // --------------------------------- 2后触发
        }
    
        const obsersver = new MutationObserver(cb) 
        // MutationObserver构造函数,cb是DOM的变化时会触发的回调函数
    
        obsersver.observe(rootDom, {
          // observe 是 obsersver实例对象的一个方法
          // observe
            // 第一个参数:表示需要观察变化的 DOM
            // 第二个参数:配置对象
          // 这里表示:观察root的变化,同级的其他dom不观察
    
          childList: true, // 观察子节点变动
    
          attributes: true, // 观察属性的变动
          attributeOldValue: true, // 观察attributes变动时,是否需要记录变动前的属性值
          attributeFilter: ['class','src'], // 表示需要观察的特定属性
    
          characterData: true, // 节点内容或节点文本的变动
          characterDataOldValue: true, // 观察characterData变动时,是否需要记录变动前的值
    
          subtree: true, // 是否将该观察器应用于该节点的所有后代节点
        })
        document.getElementById('button-dom').addEventListener('click', () => {
          rootDom.innerHTML = 'new root'
          console.log('click事件触发') // --------------------------------- 1先触发
        })
        document.getElementById('button-sibling-dom').addEventListener('click', () => {
          siblingRootDom.innerHTML = 'new sibling-root'
          console.log('click事件触发')
        })
        // 这里改变 siblingRootDom 不会触发 new MutationObserver(cb) 的 cb回调,因为 obsersver.observe() 观察的dom是root
      </script>
    </body>
    </html>
    

    Vue.nextTick 源码

    import { noop } from 'shared/util'
    import { handleError } from './error'
    import { isIE, isIOS, isNative } from './env'
    
    export let isUsingMicroTask = false
    // 标志位,是否使用 微任务队列
    
    const callbacks = []
    // callbacks 搜集 包装过后的传入 Vue.nextTick 的
      // 是这样的一个数组
      // 1. Vue.nextTick 中的第一个参数cb存在  --------------------> [() => {cb.call(ctx)}] 
      // 2. Vue.nextTick 中的第一个参数cb不存在,但支持promise -----> [() => {_resolve(ctx)}]
    
    let pending = false
    // pending 
      // 标志位,同一时间只能执行 timerFunc 函数一次
    
    
    function flushCallbacks () {
      pending = false
      const copies = callbacks.slice(0)
      // 浅拷贝 callbacks 数组,赋值给 copies
    
      callbacks.length = 0
      // 拷贝后,将原来的 callbacks 数组清空
    
      for (let i = 0; i < copies.length; i++) {
        copies[i]()
        // 遍历并调用 copies 数组中的 ( 成员函数 )
      }
    }
    
    let timerFunc
    
    if (typeof Promise !== 'undefined' && isNative(Promise)) {
      // ---------------------------------------------- 如果 prommise 并且 原生就支持promise
      const p = Promise.resolve()
      timerFunc = () => {   
        p.then(flushCallbacks)
        if (isIOS) setTimeout(noop)
        // noop 是空函数
      }
      isUsingMicroTask = true
    } else if (!isIE && typeof MutationObserver !== 'undefined' && (
      isNative(MutationObserver) ||
      // PhantomJS and iOS 7.x
      MutationObserver.toString() === '[object MutationObserverConstructor]'
    )) {
      // ---------------------------------------------- 不支持 promise 就使用 MutationObserver
      // ---------------------------------------------- 如果不是ie环境,并且 MutationObserver构造函数存在,原生支持,类型是MutationObserverConstructor
      let counter = 1
      const observer = new MutationObserver(flushCallbacks)
      // 当DOM变化时,触发 flushCallbacks
      const textNode = document.createTextNode(String(counter)) // 生成文本节点
      observer.observe(textNode, {
        // 观察 textNode 的变化,当节点内容或节点文本的变动时触发 flushCallbacks
        characterData: true, //节点内容或节点文本的变动
      })
      timerFunc = () => {
        counter = (counter + 1) % 2
        textNode.data = String(counter)
      }
      isUsingMicroTask = true
    } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
      // --------------------------------------------------- 不支持 微任务:promise
      // ---------------------------------------------- 降级:不支持 微任务:MutationObserver
      // ---------------------------------------------- 降级:宏任务:setImmediate
      timerFunc = () => {
        setImmediate(flushCallbacks)
      }
    } else {
      // Fallback to setTimeout.
      // ---------------------------------------------- 以上都不支持,降级到宏任务 setTimeout
      timerFunc = () => {
        setTimeout(flushCallbacks, 0)
      }
    }
    
    export function nextTick (cb?: Function, ctx?: Object) {
      // nextTick
        // 参数
          // cb:DOM改变触发的回调
          // ctx:上下文环境对象,即this的指向
      let _resolve
    
      callbacks.push(() => {
        if (cb) {
          try {
            cb.call(ctx)
          } catch (e) {
            handleError(e, ctx, 'nextTick')
          }
          // try catch 是为了保证 callbacks数组中一个函数出现错误,不会终止整个代码的执行
        } else if (_resolve) {
          _resolve(ctx)
          
          // new Promise(resolve => {
          //   _resolve = resolve
          // })
          
        }
      })
      // 向 callbacks 数组中push函数
      // 1. cb存在,  () => { cb.call(ctx) },只不过用 try catch 包裹了下,捕获错误
      // 2. cb不存在,() => { _resolve(ctx) }
    
      if (!pending) {
        pending = true 
        // 下一次在 timerFunc 没有执行完的时候,就不会再进入执行 timerFunc
        // 在 timerFunc => flushCallbacks => 再把pending=false
        
        timerFunc()
        // pending=false 就执行 timerFunc
      }
    
      // $flow-disable-line
      if (!cb && typeof Promise !== 'undefined') {
        // 如果 Vue.nextTick 没有提供回调函数,并且promise存在,就把 _resolve = resolve
        // 并且整个 nextTick 函数返回这个 promise 实例
        return new Promise(resolve => {
          _resolve = resolve
        })
      }
    }
    
    

    相关文章

      网友评论

          本文标题:[vue源码06] - Vue.nextTick 和 VM.$n

          本文链接:https://www.haomeiwen.com/subject/olrhnltx.html