之前写了三篇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.

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