之前写了三篇vue的源码解析,响应式,虚拟dom,和模板编译组件化.

这三篇是比较细的,这里做个总结,先看总结,再看那三篇应该会更好,

这里是把大概流程和前面的例子总结下.

一,首次渲染过程

  1. 首先我们导入vue时会初始化实例成员,静态成员

    1. 全局静态例如config,options,内部工具方法,一些静态方法例如set,nextTick,组件,指令,过滤器方法,然后原型方法例如:mount(内部调用mountComponent挂载),init,_render(方法里默认调用了options里的render,默认传递vm.$createElement提供给用户传入的render当 h函数,生成虚拟dom ,模板编译出来的render内部使用的vm._c 不用传递进去这个),_update等,
    2. 在init初始化实例成员,例如options,_isVue,uid记录, Vue.extend()初始化组件的构造函数,它继承自vue所有原型方法,合并配置options.
  2. 实例化 new Vue(),这里会调用 原型上定义的init方法;
  3. this._init()

    1. 在这里合并options配置,初始化生命周期变量,事件监听自定义事件.
    2. 执行initRender函数(生成vm._c处理编译生成render,生成vm.$createElement处理用户传入render)
    3. 执行钩子回调,对传入的data数据做响应式处理

      1. 劫持属性
      2. 生成各个属性节点的dep对象,dep对象用来通知watcher更新,并且劫持数组原型方法.
    4. 如果有计算属性生成计算watcher,有侦听器,生成侦听watcher
    5. 生成watcher时会根据传入的方法来决定是否去 取对应data中的值,如果传入方法里获取值了,会触发对应的我们前面数据劫持的get方法,从而把我们当前watcher添加到对应属性的dep的subs数组中,如果当前属性是子对象,对应子对象dep也需要添加watcher(set和数组时会用到).
    6. 然后触发created创建完成的钩子函数.
    7. 最后执行$mount挂载.
  4. vm.$mount();

    1. 这个方法会先查找options.render函数,看用户有没有传入,没有传入的话,使用传入的模板,调用compileToFunctions把模板转换成render函数,这个render函数内部调用的是vm._c来处理模板编译生成vNode
    2. 把生成的render赋值给options.render,,后续调用_render()时会从options取出render来调用,这个需要vue的编译器版本.
    3. 用户传入了render的话,后续调用_render时就会直接调用用户传入的render,从options.render上获取执行这会会使用传入的vm.$createElement来当h函数生成虚拟dom,最后调用mountComponent来进行挂载.
  5. mountComponent主要功能

    1. 定义updateComponent

      1. 这个方法作用是更新界面_update(_render()) //render中编译出的_c或 用户传入_$createElement生成虚拟dom
      2. _render()生成虚拟dom,_update()内部调用patchVnode 用来对比新旧vNode 来进行dom更新
      3. _render中会调用了对应的编译vm._c或者vm._createElement,生成虚拟dom,在这个过程中,会判断如果里面有自定义组件会调用 createComponent ,createComponent内部会调用extend()返回组件构造函数, 并且创建组件vnode, 然后注册插件init钩子,init钩子里做实例化组件,然后会调用继承自Vue的init初始化方法,最后再调用mount(),生成渲染watcher,并把组件挂载到页面上(vnode.elm,这里验证了一个组件对应一个渲染watcher)
    2. 创建渲染watcher实例,传递updateComponent

      1. 创建watcher实例时会传入updateComponent方法,这里初始化会调用传入的函数,也就是updateComponent来更新界面.
      2. 在这个过程中会 获取 我们前面data进行属性劫持中的属性,然后会触发对应的get,来把渲染watcher添加到 对应属性的dep的subs数组中.形成属性dep和渲染watcher的互相依赖.(这样就形成了一个观察关系,在这里一个渲染watcher,可能放入多个属性dep的subs数组中,因为一个渲染watcher对应一个组件, 一个属性中的dep的subs数组中也可能会放入多个不同watcher,例如同时存在渲染和计算||侦听属性的watcher
      3. 在这里声明下,一个组件对应一个渲染watcher.
  6. mounted最后执行这个钩子,整体渲染完成
  7. 到此为止 vue的首次渲染就完成了

二,响应式原理

前面讲到了,我们在new Vue()时调用init做了对数据data的劫持生成属性对应的dep发布者和对应的get和set方法,在实例化watcher时会把自身赋给Dep.target,然后获取属性值时再触发对应的get,通过dep.depend()和 childOb.dep.depend(),来把当前的watcher添加到自身和子对象的dep的subs数组中. 同时watcher也记录一下dep.id防止后续触发get时重复添加.然后改变data中的属性赋值时会触发对应的set,set会判断值是否改变,改变了的话赋给val,然后set 里会判断新赋值的值是否是对象,是的话继续进行数据劫持observe,然后调用dep的notify方法,来调用dep的subs数组中的watcher的update方法.

  • updata方法中会调用queueWatcher方法

    • 这个方法,在这里会使用watcher的id做一个对象的key来判断,是否重复,不重复的话,把当前的watcher放入queue队列中.
    • 然后来调用nextTick方法,传入flushSchedulerQueue方法当作参数

      • flushSchedulerQueue方法的作用 是按watcher.id排序watcher,也就是创建顺序(计算,侦听,渲染)排序,然后清空前面用来重复添加对象key的id,再依次执行watcher.run()
      • watche.run里执行了 this.get()也就是传入的函数, 渲染watcher的话也就是updateComponent来调用 内部的_update(_render()),来生成Vnode和对比更新.如果是计算或侦听watcher的话,执行完get()传入的方法后,会执行cb传入的回调。
      • watcher排序的作用如下:

        • 在这里首先 组件从父组件更新到子组件 也就是说假如有多个渲染watcher 先更新父的渲染watcher 后执行的子的渲染watcher
        • 其次 组件的用户监视程序在渲染监视程序之前运行 因为用户观察者在渲染观察者之前创建 ,也就是说 每一级组件的计算和侦听watcher是在渲染watcher之前执行的,因为渲染watcher中可能会用到 计算属性.
        • 最后就是如果一个组件在父组件的监视程序运行期间被销毁,它的观察者可以被跳过
    • 在这里nextTick接收到传入的函数后,生成一个匿名函数(匿名函数中执行当前传入的函数,加了try catch的错误处理)放到一个 callbacks数组中,现在它并不会立即执行callbacks数组中的函数,然后pending属性判断是false,默认是false,如果是false的话,改为true标记为本次的tick的任务,然后用Promise.resolve()生成一个promise的微任务then(flushCallbacks),挂在本次tick事件循环的最后, 在本轮tick事件循环的最后来执行微任务flushCallbacks回调,这个flushCallbacks回调的主要作用就是

      • pending状态改为false,标记本轮tick结束
      • 生成callbacks数组的副本,然后依次执行callbacks中的函数.

异步promsie 如果浏览器不支持的话会降级成setTimeout

这里也就体现了vue中的更新是异步的,批量的

这里我们用段伪代码来推理一下它的更新流程

  <div id="app">
    <p id="p" ref="p1">{{ msg }}</p>
    {{ name }}<br>
    {{ title }}<br>
  </div>
  <script src="../../dist/vue.js"></script>
  <script>
    const vm = new Vue({
      el: '#app',
      data: {
        msg: 'Hello nextTick',
        name: 'Vue.js',
        title: 'Title'
      },
      mounted() {
        this.msg = 'Hello Worlds'
        this.name = 'Hello snabbdom'
        this.title = 'Vue.js'
  
        Vue.nextTick(() => {
          console.log(this.$refs.p1.textContent)
        })
        this.msg = 'Hello'
      }
    })
  </script>
  1. 更新值,然后msg的dep.notify()//派发更新

    1. msg的dep.subs数组里的watcher.update()
    2. 执行queueWatcher方法拿到watcher.id用个对象,记录防止重复,不重复的话这个watcher放到queue队列里,(当前这个watcher是渲染watcher)
    3. 执行nextTick(flushSchedulerQueue) 然后伪代码传入的函数放入 callbacks.push(()=>{ flushSchedulerQueue() })
    4. 这时callbacks数组[执行queue中队列watcher更新的方法也就是flushSchedulerQueue,]
    5. 然后这时pending为false,改为true标记为本次的tick处理 定义一个微任务promise挂在本次tick的最后,等待将来执行(flushCallbacks是then回调函数).
  2. 更新值,然后name.dep.notify()//派发更新

    1. name的dep.subs数组里的watcher.update()
    2. 同上面一样,但是watcher.id重复,同一个渲染watcher,退出queueWatcher方法 (这里没添加重复的watcher,但是值已经更新了 val = 新值)
  3. 更新值,然后title的dep.notify()//派发更新

    1. title的dep.subs数组里的watcher.update()
    2. 同上面一样,但是watcher.id重复,同一个渲染watcher,退出queueWatcher方法(这里没添加重复的watcher,但是值已经更新了 val = 新值)
  4. Vue.nextTick(回调)

    1. 执行nextTick(回调) 然后伪代码传入的函数放入 callbacks.push(()=>{ 回调() })
    2. 这时callbacks数组[执行queue中队列watcher更新的方法也就是flushSchedulerQueue, 回调方法]
    3. 然后这时pending为true 退出
  5. 更新值,然后msg的dep.notify()//派发更新

    1. msg的dep.subs数组里的watcher.update()
    2. 同上面一样,但是watcher.id重复,同一个渲染watcher,退出queueWatcher方法(这里没添加重复的watcher,但是值已经更新了 val = 新值,这时的msg已经是Hello 而不是Hello words)
  6. 本次tick最后了来执行属于本次tick的微任务

    1. 执行flushCallbacks方法 callbacks数组中第一个方法是flushSchedulerQueue,这个方法执行queue队列中的所有watcher的更新,我们现在里面就一个渲染watcher(因为id是相同的),执行渲染wtcher.
    2. 之后执行 步骤4 Vue.nextTick传入的回调,这时渲染watcher已经执行完成了,内容已经改变了,然后执行回调再去获取对应dom的textContent时就是我们最后一次给msg赋值Hello.

这就是vue数据响应式的原理,以及它的更新过程,以及Vue.nextTick为什么能获取到更新之后的dom值

三,Vue 中模板编译的过程

我们开篇提到过,在调用$mount 挂载时 会调用compileToFunctions 把template模板转换成render函数(内部调用_c()生成虚拟dom)

这个方法主要作用:

  • 这里会首先使用parse()方法把模板转换成 ast 抽象语法树,抽象语法树是以js对象形式,用来以树形的方式描述代码结构,这个里就包含了对模板和 v-for v-if ref,等的解析.(v-for ,v-if结构化指令只能在编译阶段处理,render函数里 不会再解析模板,所以要使用js的for 和if).
  • 然后优化生成的 ast 抽象语法树 ,标记静态节点和静态根节点

    • 检测子节点是否有是纯静态节点的,一旦检测到纯静态节点,那就是永远不会更改的节点,提升为常量,重新渲染的时候不在重新创建节点
    • 在 patch 的时候直接跳过静态子树
  • 然后把抽象语法树生成字符串形式的 js 代码 这个js字符串代码是编译出来,也就是render函数代码,里面用的_c生成虚拟dom
  • 然后把字符串形式的js代码转换成js方法 赋值给凡出对象的render属性
  • 返回编译生成的render函数
  • 然后把返回的render函数赋值给options.render 后续渲染时调用的_render()就是这个render

这就是模板的编译渲染.

四,虚拟 DOM 中 Key 的作用和好处。

虚拟dom中的key 主要用来标记两个节点是否是同一个,然后做新旧vNode对比使用,对比vnode的不同来更新老vNode,从而更新 对应的dom,因为在虚拟dom节点的对比时,节点对比规则会根据key来比较,新旧开始,新旧结束,旧开始新结束,旧结束新开始.
如果都不符合然后新的开始 在老的没对比完的同级开始结束位置区间 找相同的 vnode 来跟新差异并根据需要移动位置.
这样看的话 假如没有带Key,举个例子

     <ul>
      <li v-for="value in arr" 
      :key="value"
      >{{value}}</li>
    </ul>
    [a,b,c,d]
    //更新为
    [a,x,b,c,d]

没key的时候,对比开始第一个相同,key时undefined相等,标签相同, 第2,3,4标签相同,内容不同更新dom内容,第5个生成dom 插入d
也就是说 没key时有 一个生成插入操作, 三个更新dom

有key的时候,会对比key,不会key出现undefined的情况, 根据我们上面说的规则,只需要执行一次x的生成插入操作.

从这个例子看出来,如果我们 使用的列表,如果往不同位置插入数据时,没有key的时候,更新的次数要远远大于有key的时候.所以使用列表时尽量来使用Key.

这次总结就到这里结束了.


Charon
57 声望16 粉丝

世界核平