渲染流程

模板编译的过程大抵是:Vue会把用户在<template></template>标签中写的相似于原生HTML的内容进行编译,通过一系列的逻辑解决生成渲染函数,也就是render函数,而render函数会将模板内容生成对应的VNode,而VNode再通过patch过程从而失去将要渲染的视图中的VNode,也就是DOM-DIFF的过程,最初依据VNode创立实在的DOM节点并插入到视图中, 最终实现视图的渲染更新。

所谓渲染流程,就是把用户写的相似于原生HTML的模板通过一系列解决最终反馈到视图中称之为整个渲染流程。整个流程图如下:

从图中咱们也能够看到,模板编译过程就是把用户写的模板通过一系列解决最终生成render函数的过程。对应源代码中如下代码:

export const createCompiler = createCompilerCreator(function baseCompile (  template: string,  options: CompilerOptions): CompiledResult {  // 模板解析阶段:用正则等形式解析 template 模板中的指令、class、style等数据,造成AST  const ast = parse(template.trim(), options)  if (options.optimize !== false) {    // 优化阶段:遍历AST,找出其中的动态节点,并打上标记;    optimize(ast, options)  }  // 代码生成阶段:将AST转换成渲染函数;  const code = generate(ast, options)  return {    ast,    render: code.render,    staticRenderFns: code.staticRenderFns  }})

让咱们实际以下,如下最简略的模板:

<body>    <div id="root"></div>    <script>        let vue = new Vue({            el: '#root',            template: '<div>count: {{count}}<button @click="add">加一</button></div>',            data: {                count: 0            },            methods: {                add: function(){                    this.count ++                 }            }        })    </script></body>

通过编译后生成渲染函数的后果为:

在讲述DOM-DIFF的过程之前须要关注为什么是虚构DOM?因为实在的 DOM 节点数据会占据更大的内存,当咱们频繁的去做 DOM 更新,会产生肯定的性能问题,因为 DOM 的更新有可能带来页面的重绘或重排。咱们能够用 JS 的计算性能来换取操作 DOM 所耗费的性能。最直观的思路就是咱们不要自觉的去更新视图,而是通过比照数据变动前后的状态,计算出视图中哪些地方须要更新,只更新须要更新的中央,而不须要更新的中央则不需关怀,这样咱们就能够尽可能少的操作 DOM 了。这也就是下面所说的用 JS 的计算性能来换取操作 DOM 的性能。

updateComponent = function () {    // _render()渲染函数到虚构节点    vm._update(vm._render(), hydrating);};Vue.prototype._update(vnode,prevVnode){    // __patch__()来更新新旧虚构节点    vm.$el = vm.__patch__(prevVnode, vnode);}

渲染函数到虚构DOM转换的外部的过程是怎么的呢?

render函数种的_c、_v等等对应虚构节点种不同的节点类型,Vue中一共六个节点类型,别离是:正文节点、文本节点、元素节点、组件节点、函数式组件节点、克隆节点。

咱们来看看上述的渲染函数生成的虚构DOM:

当咱们点击按钮后生成的虚构DOM:

两个虚构DOM之间的比照,也叫DOM-DIFF算法,采纳深度优遍历,示意图如下:

整个patch无非就是干三件事:①创立节点:新的VNode中有而旧的oldVNode中没有,就在旧的oldVNode中创立。②删除节点:新的VNode中没有而旧的oldVNode中有,就从旧的oldVNode中删除。③更新节点:新的VNode和旧的oldVNode中都有,就以新的VNode为准,更新旧的oldVNode。

如果是雷同的Vnode就要进行子节点的比照,示意图如下的动画:

VUE的DIFF算法的外围就是updateChildren办法,它是一个基于链表的双向比照。其中两种穿插的状况,旧节点链表的头和新节点链表的尾如果雷同,就须要把老节点链表的头放到老节点链表的尾下来,旧节点链表的尾和新节点链表的头如果雷同,同理,将旧节点链表的尾放在旧节点链表的头上去,可见其比照的后果是基于老节点链表上的。如果都不等,遍历老节点上的key,如果新节点链表的头的key在老节点的key的MAP中找到,就把新节点链表的头放在旧节点链表的头上去,如果不存在key,那就在旧节点链表的头上新建一个新节点链表的头。遍历实现后,新节点链表上存在的旧节点链表上不存在就批量更新到旧节点上,反之,批量删除。

最初一步就是虚构DOM怎么转换成实在的DOM,有两种形式,初始化的时候间接挂载,另一种点击按钮后虚构节点进行比照。

先看看首次挂载:

if (!prevVnode) {    // initial render    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);} else {    // updates    vm.$el = vm.__patch__(prevVnode, vnode);}function createElm () {    vnode.elm = nodeOps.createElement(tag, vnode);    {        createChildren(vnode, children, insertedVnodeQueue);        insert(parentElm, vnode.elm, refElm);    }}// 还是通过document.createElement来创立实在的DOMfunction createElement (tagName, vnode) {   var elm = document.createElement(tagName);   if (tagName !== 'select') {       return elm   }}

点击按钮后,count变为1,DOM-DIFF找到要更新的节点间接取代这个文本节点:

// node曾经是实在存在的node,并且只想页面曾经渲染的文档function setTextContent (node, text) {    node.textContent = text;}

至此,咱们实现了整个编译到渲染的过程。

变动侦听

vue中通过Observer类来作用于对象,对象能够通过Object.defineProperty办法将数据属性变更为拜访器属性,拜访器属性中的getter和setter能够实现对对象变动侦测,Vue中递归的把一个对象每个属性通过这种形式变为可侦测的。如下示意代码:

Object.defineProperty(obj, key, {      enumerable: true,      configurable: true,      get: function reactiveGetter() {console.log('我被读取了')},      set: function reactiveSetter(newVal) {console.log('我被批改了')}});

但对于数组不存在这个办法,Vue采纳拦挡数组办法的形式来侦测数组的变动,具体的做法是采纳寄生模式来实现数组办法拦挡,如下伪代码push办法示例:

const arrayMethods = Object.create(Array.prototype)const original = Array.prototype.pushdef(arrayMethods, 'push', function mutator () {    var result = Array.prototype.push.apply(arrayMethods, args);    dep.notify()     return result});array.__proto__ = arrayMethods;//如果不能用隐式原型链链接,那么vue间接复制一个办法正本到数组实例上。

Object.defineProperty办法无奈侦测到对象属性的新增和删除,Vue中的数组拦截器只是简略的拦挡作用于数组自身的几种办法,而对于罕用的索引操作也无能为力。针对这些弊病,Vue提供几个静态方法和实例办法来补救侦听的有余。针对对象其具体做法重新加入侦听零碎或者删除该属性而后告诉依赖更新,针对数组其具体做法是应用拦截器中的splice办法实现数组元素的增删。

依赖生命周期

Vue在初始化状态时,会将数据退出变动侦听零碎,将computed和watch中的办法遍历生成一个个办法依赖,在实例挂载生成独一份的视图依赖,这里有个经典的问题就是Vue有了响应式零碎,为什么还须要虚构DOM,Vue能够将视图中的每一个数据对应一份依赖,每一份依赖对应着一个DOM级别的改变,Vue侦听的数据一旦扭转那么就会告诉数据对应的依赖进行简洁的DOM更新。Vue1.0版本正是这样做的,DOM级别的细粒度,但这样做会生成很多的依赖带来了内存的开销,因而2.0引入了虚构DOM,将粒度改为组件级别,这也就时为什么只生成独一份的视图依赖,它的表达式是function({vm._update(vm._render(), hydrating)},从代码表达式也看得出来组件实例的render和update流程。

依赖是视图和模型之间的桥梁,变动侦测起到了触发器的作用,因而在变动侦测中收集依赖和告诉依赖更新,一旦数据被渲染函数用到那么就会触发getter,收集依赖。同理,数据被扭转,触发setter,告诉该数据关联的依赖更新视图。

Vue通过Dep类来治理依赖,因为对象和数组变动侦测的形式不同,依赖的收集都是在getter中实现,而告诉依赖更新是以不同的形式引入Dep类从而告诉相干的依赖进行更新操作。对象是在defineReactive中拜访Dep类,数组拦截器的依赖管理器的引入是间接绑定在每个value的__ob__属性上,如下伪代码:

class Observe(){    constructor(){        this.dep = new Dep();        def(value, '__ob__', this);    }}new Observe(value)// 在数组中告诉依赖更新def(arrayMethods, 'push', function mutator () {    var result = Array.prototype.push.apply(arrayMethods, args);    this.__ob__.dep.notify()    return result});

视图依赖存在有个newDeps属性,用来寄存以后视图上每个数据对应的依赖数组dep,当一个数据扭转后,将会从依赖视图中的newDeps中寻找该数据对应的dep,一一更新依赖。当更改一个数据,它不肯定只在视图中体现,还有其余中央,如computed和watch中也会触发,因而收集依赖不只是收集以后视图的依赖,而且还收集computed和watch的依赖。

当一个数据扭转时,触发function({vm._update(vm._render(), hydrating)}从新渲染视图,从新执行依赖收集,将收集的依赖数据放在newDeps中。视图依赖还存在一个deps属性,是用来存在上一次视图render时收集的每个数据对应的依赖数组dep。vue在每次依赖收集实现之后都会去革除旧的依赖,即执行cleanupDeps办法,这个过程也叫革除依赖。它会首先遍历deps,移除对dep的订阅,而后把newDeps和deps 替换,并把newDeps清空。那么为什么须要做 deps 订阅的移除呢,在增加 deps的订阅过程,曾经能通过 id 去重防止反复订阅了。思考到一种场景,咱们的模板会依据 v-if 去渲染不同子模板,当应用新的子模板时批改了不可见模板的数据,会告诉到不可见模板数据的notify,这显然是有节约的。因而 Vue 设计了在每次增加完新的订阅,会移除掉旧的订阅,这样就保障了在方才的场景中,如果渲染新模板的时候去批改不可见模板的数据,不可见数据的依赖曾经被移除了,所以不会有任何节约。最初在组件销毁阶段,会移除所有的依赖,即移除deps中的依赖和vm._watchers中的依赖,也叫卸载依赖。整个依赖生命周期的操作都在Watcher的原型上提现,如下代码:

class Watcher{    constructor(expOrFn){        this.deps = [];        this.newDeps = [];        this.expression = expOrFn;        this.get()//首次渲染生成视图依赖    }    get(){        value = this.expression.call(vm, vm);//render中依赖收集        this.cleanupDeps();//再次渲染时革除旧的依赖        return value    }    cleanupDeps(){}//革除旧的依赖    update(){}//更新依赖    depend () {}//收集存在以后依赖的所有的dep实例    teardown () {}//卸载所有依赖}

实例生命周期

依赖的生命周期是建设在实例的生命周期上,实例生命周期能够对整个Vue运行机制的每个阶段有着准确把握。在new Vue之前vue会将一些全局的API和属性绑定为Vue类的静态方法和实例办法,在new Vue之后首先初始化该实例上的生命周期和事件以及渲染,便开始执行执行beforeCreate钩子函数。紧接着初始化state,InitState将配置上传入的props,methods等数据成为新创建的实例的属性,将data退出侦听零碎,将computed和watch中的办法创立成一个个依赖实例。而后执行created钩子函数,宣告实例正式创立,接下来,vue完整版会存在模板编译阶段,该阶段将模板编程渲染函数,而运行时版本间接间接调用render,进入挂载阶段,执行beforeMount钩子函数,在挂载实现之前,即执行mounted钩子函数之前,须要通过render渲染出虚构DOM并且挂载到页面上,同时生成惟一一份视图依赖并开启对数据的监控,那么这样就能够在数据状态变动时告诉依赖更新。伪代码如下:

callhook(vm,'beforeMount')var updateComponent = function (){vm._update(vm._render(), hydrating)};var before = function before () {callHook(vm, 'beforeUpdate')}class Watcher{    constructor(updateComponent,before){        updateComponent.call(vm)         this.before = before    }}new Watcher()callHook(vm, 'mounted')

new Watcher驱动vue渲染出虚构DOM并实现挂载的过程中,render函数中会拜访到data中的数据,触发getter收集每个数据对应的依赖,当该数据发生变化时就会告诉与之相干的依赖,依赖接管到告诉后就会一一调用beforeUpdate钩子函数去更新视图,视图更新实现之后,开始一一的调用updated钩子函数。伪代码如下:

function flushSchedulerQueue(){    for (index = 0; index < queue.length; index++) {        watcher = queue[index];        callHook(vm, 'beforeUpdate')         updateComponent.call(vm)    }     callUpdatedHooks(queue.slice(0));}function callUpdatedHooks (queue) {      var i = queue.length;      while (i--) {          var watcher = queue[i];          callHook(vm, 'updated');      }}

最初的销毁阶段beforeDestroy把本人从父级实例的子实例列表中删除和删除所有的依赖并驱动视图更新,而后调用destroyed钩子函数,接下来就开始将本人身上的事件监听移除和vue实例指向移除。伪代码如下:

Vue.prototype.$destroy = function () {    callHook(vm, 'beforeDestroy');    remove(parent.$children, vm);//从父实例上移除    while (i--) {          vm._watchers[i].teardown();//删除所有依赖    }    vm.__patch__(vm._vnode, null);//驱动视图更新    callHook(vm, 'destroyed');    vm.$off();//移除监听    vm.$el.__vue__ = null;//实例置空}

如果存在父子组件,以上剖析能够很简略得出父子组件之间的生命周期程序,能够分为四个阶段:加载渲染阶段、子组件更新阶段、父组件更新阶段和销毁阶段。加载渲染阶段:父beforeCreate ---> 父created ---> 父beforeMount ---> 子beforeCreate ---> 子created ---> 子beforeMount ---> 子mounted ---> 父mounted。子组件更新阶段:父beforeUpdate ---> 子beforeUpdate ---> 子updated ---> 父updated。父组件更新阶段:父beforeUpdate ---> 父updated。销毁阶段:父beforeDestroy ---> 子beforeDestroy ---> 子destroyed ---> 父destroyed。

异步更新队列

可能你还没有留神到,Vue 在更新 DOM 时是异步执行的。只有侦听到数据变动,Vue 将开启一个队列,并缓冲在同一事件循环中产生的所有数据变更。如果同一个 watcher 被屡次触发,只会被推入到队列中一次。这种在缓冲时去除反复数据对于防止不必要的计算和 DOM 操作是十分重要的。而后,在下一个的事件循环“tick”中,Vue 刷新队列并执行理论 (已去重的) 工作。Vue 在外部对异步队列尝试应用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不反对,则会采纳 setTimeout(fn, 0) 代替。

例如,当你设置 vm.someData = 'new value',该组件不会立刻从新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。少数状况咱们不须要关怀这个过程,然而如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些辣手。尽管 Vue.js 通常激励开发人员应用“数据驱动”的形式思考,防止间接接触 DOM,然而有时咱们必须要这么做。为了在数据变动之后期待 Vue 实现更新 DOM,能够在数据变动之后立刻应用 Vue.nextTick(callback)。这样回调函数将在 DOM 更新实现后被调用。例如:

<div id="example">{{message}}</div>var vm = new Vue({  el: '#example',  data: {    message: '123'  }})vm.message = 'new message' // 更改数据vm.$el.textContent === 'new message' // falseVue.nextTick(function () {  vm.$el.textContent === 'new message' // true})

运行机制总览图

最初看看运行机制总览图。