vue 2.0 源码学习概览

259 阅读13分钟

引言

虽然Vue3.0 已经问世很久,但个人认为如果是PC应用,除非IE彻底消失,否则也很难大面积都普及。

随着工作年限的增长,听到“源码”的学习越来越频繁,本篇文章就大概介绍下Vue2.0 的源码概览(只看过一遍,请大家多多指教。每个部分的详细解析后续不定期交作业)

话不多说,正文来啦~~~

一.变化侦测

Vue是数据驱动视图,就是指当数据发生变化时,或是因为用户操作引起,或是因为后端数据发生改变时,页面也随之改变。

变化侦测就是追踪数据的状态,一旦状态发生了改变,就去更新视图。

1.Object的变化侦测

1.1 Object.defineProperty

使用:

/*
    obj: 目标对象
    prop: 需要操作的目标对象的属性名
    descriptor: 描述符
    
    return value 传入对象
*/
Object.defineProperty(obj, prop, descriptor)

descriptor的一些属性:

  • enumerable,属性是否可枚举,默认 false。

  • configurable,属性是否可以被修改或者删除,默认 false。

  • get,获取属性的方法。

  • set,设置属性的方法。


举个栗子(每当该属性进行读或写操作的时候就会触发get()和set())

let car = {}
let val = 3000
Object.defineProperty(car, 'price', {
  enumerable: true,
  configurable: true,
  get(){
    console.log('price属性被读取了')
    return val
  },
  set(newVal){
    console.log('price属性被修改了')
    val = newVal
  }
})

1.2 实现 observer

class Vue {
    /* vue构造类 */
    constructor(value) {
        this.value = value;
        // 给value新增一个__ob__属性,值为该value的Observer实例
        // 相当于为value打上标记,表示它已经被转化成响应式了,避免重复操作
        def(value,'__ob__',this)
        if(Array.isArray(value)) {
            // 当value为数组时的逻辑
            // ...
        } else {
            observer(value)
        }
    }
}

/*通过遍历所有属性的方式对该对象的每一个属性都通过 defineReactive 处理, 实际上observer会进行递归调用*/
function observer (value) {
    if (!value || (typeof value !== 'object')) {
        return;
    }
    
    Object.keys(value).forEach((key) => {
        defineReactive(value, key, value[key]);
    });
}

/*通过 Object.defineProperty来实现对对象的响应式化*/
function defineReactive (obj, key, val) {
    // 如果只传了obj和key,那么val = obj[key]
    if (arguments.length === 2) {
        val = obj[key]
    }
    if(typeof val === 'object'){
        new Observer(val)
    }
    Object.defineProperty(obj, key, {
        enumerable: true,       /* 属性可枚举 */
        configurable: true,     /* 属性可被修改或删除 */
        get: function reactiveGetter () {
            console.log(`${key}属性被读取了`);
            return val;
        },
        set: function reactiveSetter (newVal) {
            if (newVal === val) return;
            console.log(`${key}属性被修改了`);
            val = newVal;
            cb(newVal); /* 更新视图 */
        }
    });
}

1.3 依赖收集

1.3.1 什么是依赖收集

当数据发生改变时我们需要更新视图,但不能只要数据发生改动就更新整个视图,所以最好是某个部分依赖了这个数据,这个部分更新视图即可,这个过程就叫做依赖收集。

1.3.2 为什么要依赖收集

1)有些数据的改变,在视图中并不需要,没必要更新视图;

2)一些全局定义的对象,在多个Vue对象中用到需要展示,当全局定义的对象发生改变时,所有用到它的地方都应更新。

1.3.3 什么时候依赖收集

在getter中收集依赖,在setter中通知依赖更新。

1.3.4 把依赖收集到哪里(依赖管理器Dep类)

在对象被读时,触发getter,把当前的Watcher对象(存放在 Dep.target中)收集到Dep类中去;

当对象被写时,触发setter,通知 Dep类调用notify来触发所有Watcher对象的update方法更新对应视图。

function defineReactive (obj, key, val) {
    if (arguments.length === 2) {
        val = obj[key]
    }
    if(typeof val === 'object'){
        new Observer(val)
    }
    /* 一个Dep类对象 */
    const dep = new Dep();
    
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
            /* 将Dep.target(即当前的Watcher对象存入dep的subs中) */
            dep.depend(); // 收集依赖
            return val;         
        },
        set: function reactiveSetter (newVal) {
            if (newVal === val) return;
            /* 在set的时候触发dep的notify来通知所有的Watcher对象更新视图 */
            dep.notify();
        }
    });
}

class Vue {
    constructor(options) {
        this._data = options.data;
        observer(this._data);
        /* 新建一个Watcher观察者对象,这时候Dep.target会指向这个Watcher对象 */
        new Watcher();
        /* 在这里模拟render的过程,为了触发test属性的get函数 */
        console.log('render~', this._data.test);
    }
}

订阅者 Dep - 依赖管理器

export default class Dep {
  constructor () {
    this.subs = []
  }

  addSub (sub) {
    this.subs.push(sub)
  }
  // 删除一个依赖
  removeSub (sub) {
    remove(this.subs, sub)
  }
  // 添加一个依赖
  depend () {
    if (window.target) {
      this.addSub(window.target)
    }
  }
  // 通知所有依赖更新
  notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
1.3.5 依赖到底是谁

谁用到了数据,谁就是依赖,我们就为谁创建一个Watcher实例,在创建Watcher实例的过程中会自动的把自己添加到这个数据对应的依赖管理器中,以后这个Watcher实例就代表这个依赖,当数据变化时,我们就通知Watcher实例,由Watcher实例再去通知真正的依赖。

class Watcher {
    constructor () {
        /* 在new一个Watcher对象时将该对象赋值给Dep.target,在get中会用到 */
        Dep.target = this;
    }

    /* 更新视图的方法 */
    update () {
        console.log("视图更新啦~");
    }
}

Dep.target = null;

Watcher先把自己设置到全局唯一的指定位置(window.target),然后读取数据。因为读取了数据,所以会触发这个数据的getter。接着,在getter中就会从全局唯一的那个位置读取当前正在读取数据的Watcher,并把这个watcher收集到Dep中去。收集好之后,当数据发生变化时,会向Dep中的每个Watcher发送通知。通过这样的方式,Watcher可以主动去订阅任意一个数据的变化。

1.4 目前存在的问题

向object数据里添加一对新的key/value或删除一对已有的key/value时,它是无法观测到的,导致当我们对object数据添加或删除值时,无法通知依赖,无法驱动视图进行响应式更新。

解决方案:Vue.set和Vue.delete

1.5 总结

整个流程大致为:

1)Data通过observer转换成了getter/setter的形式来追踪变化。

2)当外界通过Watcher读取数据时,会触发getter从而将Watcher添加到依赖中。

3)当数据发生了变化时,会触发setter,从而向Dep中的依赖(即Watcher)发送通知。

4)Watcher接收到通知后,会向外界发送通知,变化通知到外界后可能会触发视图更新,也有可能触发用户的某个回调函数等。

2.Array的变化侦测

对于Object数据我们使用的是JS提供的对象原型上的方法Object.defineProperty,而这个方法是对象原型上的,所以Array无法使用这个方法,所以我们需要对Array型数据设计一套另外的变化侦测机制。

Array本质上也是Object:

let arr = [1,2,3]
// =>
let arrObj = {
    "0":1,
    "1":2,
    "2":3
}

数组arr的索引值恰好就是arrObj的key值,所以我们通过数组的索引值来操作数组时是可以用Object.defineProperty监测到的。但是,数组并不是只能由索引值来操作数组,更常用的操作数组的方法是使用数组原型上的一些方法如push,shift等来操作数组,当使用这些数组原型方法来操作数组时,Object.defineProperty就监测不到了,所以Vue对Array型数据单独设计了数据监测方式。

2.1 在哪里收集依赖

在getter中的Observer类中

2.2 使Array型数据可观测

Object的变化时通过setter来追踪的,只有某个数据发生了变化,就一定会触发这个数据上的setter。但是Array型数据没有setter,Vue就通过重写数组的一些方法来达到setter的功能。

经过整理,Array原型中可以改变数组自身内容的方法有7个,分别是:push,pop,shift,unshift,splice,sort,reverse。

const arrayProto = Array.prototype
// 创建一个对象作为拦截器
export const arrayMethods = Object.create(arrayProto)

// 改变数组自身内容的7个方法
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
 methodsToPatch.forEach(function (method) {
  const original = arrayProto[method]      // 缓存原生方法
  Object.defineProperty(arrayMethods, method, {
    enumerable: false,
    configurable: true,
    writable: true,
    value:function mutator(...args){
      const result = original.apply(this, args)
      return result
    }
  })
})

2.3 深度侦测(侦测数据中所有子数据的变化)

export class Observer {
  value: any;
  dep: Dep;

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)   // 将数组中的所有元素都转化为可被侦测的响应式
    } else {
      this.walk(value)
    }
  }
 /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

export function observe (value, asRootData){
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else {
    ob = new Observer(value)
  }
  return ob
}

对于Array型数据,调用了observeArray()方法,该方法内部会遍历数组中的每一个元素,然后通过调用observe函数将每一个元素都转化成可侦测的响应式数据。

2.4 数组新增元素的侦测

拿到新增的这个元素,然后调用observe函数将其转化即可。我们知道,可以向数组内新增元素的方法有3个,分别是:push、unshift、splice。我们只需对这3中方法分别处理,拿到新增的元素,再将其转化即可:

methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args   // 如果是push或unshift方法,那么传入参数就是新增的元素
        break
      case 'splice':
        inserted = args.slice(2) // 如果是splice方法,那么传入参数列表中下标为2的就是新增的元素
        break
    }
    if (inserted) ob.observeArray(inserted) // 调用observe函数将新增的元素转化成响应式
    // notify change
    ob.dep.notify()
    return result
  })
})

2.5 目前存在的问题

通过数组的下标来操作数据时,是无法被检测到的。

解决方案:Vue.set和Vue.delete

二.虚拟DOM

1.什么是虚拟DOM?

我们把组成一个DOM节点的必要东西通过一个JS对象表示出来,那么这个JS对象就可以用来描述这个DOM节点,我们把这个JS对象(VNode类)就称为是这个真实DOM节点的虚拟DOM节点。

VNode类可以描述6种类型的节点:注释节点、文本节点、元素节点、组件节点、函数式组件节点、克隆节点。

2.为什么要有虚拟DOM?

1)真实的dom属性很多,操作真实的dom比较慢,只要改一个数据,会引起整个dom树重绘;操作虚拟dom可以保证不管数据变化多少,每次重绘的性能都可以接受,可以按需修改发生变化的部分——以JS的计算性能来换取操作真实DOM所消耗的性能

2)使用虚拟 DOM也能使得Vue不再依赖于浏览器环境。我们可以很容易的在 Broswer 端或者服务器端操作虚拟 DOM, 需要 render 时再将虚拟 DOM 转换为真实 DOM 即可。这也使得 Vue 有了实现服务器端渲染的能力。

3.虚拟DOM有什么坏处吗?

1)无法进行极致优化: 虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。首次渲染大量 DOM 时,由于多了一层虚拟 DOM 的计算,会比 innerHTML 插入慢。

  1. 需要额外的创建函数,如 createElementh,但可以通过 JSX 或者 vue-loader 来简化成 XML 写法。但是这么做会依赖打包工具。

4.Vue的DOM-Diff

在Vue中,把 DOM-Diff过程叫做patch过程:以新的VNode为基准,改造旧的oldVNode使之成为跟新的VNode一样。

先上图:

patch2.png

整个patch过程其实就是做3件事:

  • 创建节点:新的VNode中有而旧的oldVNode中没有,就在旧的oldVNode中创建。
  • 删除节点:新的VNode中没有而旧的oldVNode中有,就从旧的oldVNode中删除。
  • 更新节点:新的VNode和旧的oldVNode中都有,就以新的VNode为准,更新旧的oldVNode

diff 算法是通过同层的树节点进行比较而非对树进行逐层搜索遍历的方式,所以时间复杂度只有 O(n)

4.1 创建节点

只有3种类型的节点能够被创建并插入到DOM中,它们分别是:元素节点、文本节点、注释节点。

function createElm (vnode, parentElm, refElm) {
    const data = vnode.data
    const children = vnode.children
    const tag = vnode.tag
    if (isDef(tag)) {
      vnode.elm = nodeOps.createElement(tag, vnode)   // 创建元素节点
      createChildren(vnode, children, insertedVnodeQueue) // 创建元素节点的子节点
      insert(parentElm, vnode.elm, refElm)       // 插入到DOM中
    } else if (isTrue(vnode.isComment)) {
      vnode.elm = nodeOps.createComment(vnode.text)  // 创建注释节点
      insert(parentElm, vnode.elm, refElm)           // 插入到DOM中
    } else {
      vnode.elm = nodeOps.createTextNode(vnode.text)  // 创建文本节点
      insert(parentElm, vnode.elm, refElm)           // 插入到DOM中
    }
  }

4.2 删除节点

如果某些节点在新的VNode中没有而在旧的oldVNode中有,那么就需要把这些节点从旧的oldVNode中删除:在要删除节点的父元素上调用removeChild方法

function removeNode (el) {
  const parent = nodeOps.parentNode(el)  // 获取父节点
  if (isDef(parent)) {
    nodeOps.removeChild(parent, el)  // 调用父节点的removeChild方法
  }
}

4.3 更新节点

function patchVnode (oldVnode, vnode) {
    /* 新老VNode相同不做处理,return掉 */
    if (oldVnode === vnode) {
        return;
    }
    
    /* 如果新老节点都是静态的,并且key相同时,直接取老节点的ele及componentInstance就可 */
    /* 是否是静态的节点,在编译(optimize)的过程会直接标记出来*/
    if (vnode.isStatic && oldVnode.isStatic && vnode.key === oldVnode.key) {
        vnode.elm = oldVnode.elm;
        vnode.componentInstance = oldVnode.componentInstance;
        return;
    }

    const elm = vnode.elm = oldVnode.elm;
    const oldCh = oldVnode.children;
    const ch = vnode.children;

    /* 如果新的VNode节点是文本节点,直接用setTextContent */
    if (vnode.text) {
        /* nodeOps是一个适配层,根据不同平台提供不同的操作平台 DOM 的方法,实现跨平台 */
        nodeOps.setTextContent(elm, vnode.text);
    } else {
        if (oldCh && ch && (oldCh !== ch)) {
            /* 都存在且不相同 */
            updateChildren(elm, oldCh, ch);
        } else if (ch) {
            /* 只有ch存在*/
            if (oldVnode.text) {
                /* 如果老节点是文本节点,需先将节点的文本清除 */ 
                nodeOps.setTextContent(elm, '');
            }
            addVnodes(elm, null, ch, 0, ch.length - 1);
        } else if (oldCh) {
            /* 如果只有oldCh,需要清除掉所有的老节点 */
            removeVnodes(elm, oldCh, 0, oldCh.length - 1)
        } else if (oldVnode.text) {
            /* 只有老节点是文本节点时,清除节点文本内容*/
            nodeOps.setTextContent(elm, '')
        }
    }
}

4.4 更新子节点

当新的VNode与旧的oldVNode都是元素节点并且都包含子节点时,那么这两个节点的VNode实例上的children属性就是所包含的子节点数组。我们把新的VNode上的子节点数组记为newChildren,把旧的oldVNode上的子节点数组记为oldChildren。

同样是有四种情况:

  • 创建子节点:如果newChildren里面的某个子节点在oldChildren里找不到与之相同的子节点,那么说明newChildren里面的这个子节点是之前没有的,是需要此次新增的节点,那么就创建子节点。
  • 删除子节点:如果把newChildren里面的每一个子节点都循环完毕后,发现在oldChildren还有未处理的子节点,那就说明这些未处理的子节点是需要被废弃的,那么就将这些节点删除。
  • 移动子节点:如果newChildren里面的某个子节点在oldChildren里找到了与之相同的子节点,但是所处的位置不同,这说明此次变化需要调整该子节点的位置,那就以newChildren里子节点的位置为基准,调整oldChildren里该节点的位置,使之与在newChildren里的位置相同。
  • 更新节点:如果newChildren里面的某个子节点在oldChildren里找到了与之相同的子节点,并且所处的位置也相同,那么就更新oldChildren里该节点,使之与newChildren里的该节点相同。

4.5 优化更新子节点

外层循环新节点的children数组,内层循环旧节点的children数组,每循环外层的新节点的children数组中的一个子节点,就去内层老节点的children数组中去找有没有相同的节点,再根据不同的情况去做操作。当包含的子节点数量过多时,这样循环算法的时间复杂度就会变的很大,不利于性能提升。

优化:

  • 先把newChildren数组里的所有未处理子节点的第一个子节点oldChildren数组里所有未处理子节点的第一个子节点做比对,如果相同,那就直接进入更新节点的操作;
  • 如果不同,再把newChildren数组里所有未处理子节点的最后一个子节点oldChildren数组里所有未处理子节点的最后一个子节点做比对,如果相同,那就直接进入更新节点的操作;
  • 如果不同,再把newChildren数组里所有未处理子节点的最后一个子节点oldChildren数组里所有未处理子节点的第一个子节点做比对,如果相同,那就直接进入更新节点的操作,更新完后再将oldChildren数组里的该节点移动到与newChildren数组里节点相同的位置;
  • 如果不同,再把newChildren数组里所有未处理子节点的第一个子节点和oldChildren数组里所有未处理子节点的最后一个子节点做比对,如果相同,那就直接进入更新节点的操作,更新完后再将oldChildren数组里的该节点移动到与newChildren数组里节点相同的位置;
  • 最后四种情况都试完如果还不同,那就按照之前循环的方式来查找节点。

三.模板编译

1.什么是模板编译

Vue会把用户在<template></template> 标签中写的类似于原生HTML的内容进行编译,把原生HTML的内容找出来,再把非原生HTML找出来,经过一系列的逻辑处理生成渲染函数,也就是render函数,而render函数会将模板内容生成对应的VNode,而VNode再经过的patch过程从而得到将要渲染的视图中的VNode,最后根据VNode创建真实的DOM节点并插入到视图中,最终完成视图的渲染更新。

把用户在<template></template>标签中写的类似于原生HTML的内容进行编译,把原生HTML的内容找出来,再把非原生HTML找出来,经过一系列的逻辑处理生成渲染函数,也就是render函数的这一段过程称之为模板编译过程

2.整体流程

编译.png

3.模板编译内部流程

将一堆字符串模板解析成抽象语法树AST后,我们就可以对其进行各种操作处理了,处理完后用处理后的AST来生成render函数。其具体流程可大致分为三个阶段:

  • 模板解析阶段:解析器 —— 将一堆模板字符串用正则等方式解析成抽象语法树AST;
  • 优化阶段:优化器 —— 遍历AST,找出其中的静态节点,并打上标记;
  • 代码生成阶段:代码生成器 —— 将AST转换成渲染函数

模板解析其实就是根据被解析内容的特点使用正则等方式将有效信息解析提取出来,根据解析内容的不同分为HTML解析器,文本解析器和过滤器解析器。而文本信息与过滤器信息又存在于HTML标签中,所以在解析器主线函数parse中先调用HTML解析器parseHTML函数对模板字符串进行解析,如果在解析过程中遇到文本或过滤器信息则再调用相应的解析器进行解析,最终完成对整个模板字符串的解析。

4.模板解析阶段(HTML解析器)

工作流程:一边解析不同的内容一边调用对应的钩子函数生成对应的AST节点,最终完成将整个模板字符串转化成AST。

在解析器内维护了一个栈,用来保证构建的AST节点层级与真正DOM层级一致。

5.模板解析阶段(文本解析器)

  • 判断传入的文本是否包含变量
  • 构造expression
  • 构造tokens

文本解析器的作用就是将HTML解析器解析得到的文本内容进行二次解析,解析文本内容中是否包含变量,如果包含变量,则将变量提取出来进行加工,为后续生产render函数做准备。

6.优化

  • 在AST中找出所有静态节点并打上标记;
  • 在AST中找出所有静态根节点并打上标记;

7.代码生成阶段,到底是要生成什么代码?

要生成render函数字符串

8.模板编译的最终目的是什么?

把用户所写的模板转化成供Vue实例在挂载时可调用的render函数。或者你可以这样简单的理解为:模板编译就是一台机器,给它输入模板字符串,它就输出对应的render函数。

四.生命周期

1. 初始化

new Vue():合并配置,调用一些初始化函数,触发生命周期钩子函数,调用$mount开启下一个阶段。

initLifecycle:给vue实例初始化了一些属性,包括以$开头的供用户使用的外部属性,也包括以_开头的供内部使用的内部属性。

initEvents: 初始化实例的事件系统。初始化事件函数initEvents实际上初始化的是父组件在模板中使用v-on或@注册的监听子组件内触发的事件。

initInjections:初始化inject选项。

initState:我们在data中可以使用props,在watch中可以观察data和props,之所以可以这样做,就是因为在初始化的时候遵循了这种顺序,先初始化props,接着初始化data,最后初始化watch。

  • initProps
  • initMethods
  • initData
  • initComputed
  • initWatch

这5个选项中的所有属性最终都会被绑定到实例上,这也就是我们为什么可以使用this.xxx来访问任意属性。同时正是因为这一点,这5个选项中的所有属性名都不应该有所重复,这样会造成属性之间相互覆盖。

2. 模板编译阶段

完整版的$mount

  • 根据传入的el参数获取DOM元素;
  • 在用户没有手写render函数的情况下获取传入的模板template;
  • 将获取到的template编译成render函数;

只包含运行时的$mount

只包含运行时的版本拥有创建Vue实例、渲染并处理Virtual DOM等功能,基本上就是除去编译器外的完整代码。 获取到el选项对应的DOM元素后直接调用mountComponent函数进行挂载操作。

3. 挂载阶段

创建Vue实例并用其替换el选项对应的DOM元素,同时还要开启对模板中数据(状态)的监控,当数据(状态)发生变化时通知其依赖进行视图更新。

我们将挂载阶段所做的工作分成两部分进行了分析,第一部分是将模板渲染到视图上,第二部分是开启对模板中数据(状态)的监控。两部分工作都完成以后挂载阶段才算真正的完成了。

4.销毁阶段

当调用了实例上的vm.$destory方法后,实例就进入了销毁阶段,在该阶段所做的主要工作是将当前的Vue实例从其父级实例中删除,取消当前实例上的所有依赖追踪并且移除实例上的所有事件监听器。

五.实例方法

1.数据相关的方法:

vm.$watch; ②vm.$set; ③vm.$delete

2.事件相关的方法:

vm.$on; ②vm.$emit; ③vm.$off; ④vm.$once

3.生命周期相关的方法:

vm.$mount; ②vm.$forceUpdate; ③vm.$nextTick; ④vm.$destory

六.全局API

实例方法是将方法挂载到Vue的原型上,而全局API是直接在Vue上挂载方法。在Vue中,全局API一共有12个,分别是Vue.extend、Vue.nextTick、Vue.set、Vue.delete、Vue.directive、Vue.filter、Vue.component、Vue.use、Vue.mixin、Vue.observable、Vue.version。

七.过滤器

1.定义

过滤器的定义有两种方式:

  • 在组件选项内定义:为本地过滤器,它只能用于当前组件中;
  • 使用全局APIVue.filter定义全局过滤器:全局过滤器,它可以用在任意组件中。

2.使用

过滤器不仅可以单个使用,还可以多个串联一起使用。当多个过滤器串联一起使用的时候,前一个过滤器的输出是后一个过滤器的输入,通过将多种不同的过滤器进行组合使用来将文本处理成最终需要的格式。

3.工作原理

将用户写在模板中的过滤器通过模板编译,编译成_f函数的调用字符串,之后在执行渲染函数的时候会执行_f函数,从而使过滤器生效。

_f函数其实就是resolveFilter函数的别名,在resolveFilter函数内部是根据过滤器id从当前实例的$options中的filters属性中获取到对应的过滤器函数,在之后执行渲染函数的时候就会执行获取到的过滤器函数。

4.解析过滤器

调用过滤器解析器parseFilters函数进行解析。parseFilters函接收一个形如'message | capitalize'这样的过滤器字符串作为,最终将其转化成_f("capitalize")(message)输出。在parseFilters函数的内部是通过遍历传入的过滤器字符串每一个字符,根据每一个字符是否是一些特殊的字符从而作出不同的处理,最终,从传入的过滤器字符串中解析出待处理的表达式expression和所有的过滤器filters数组。最后,将解析得到的expression和filters数组通过调用wrapFilter函数将其构造成_f函数调用字符串。

八.指令

1.方式

  • 使用全局API——Vue.directive来定义全局指令:被存放在Vue.options['directives']中;
  • 在组件内的directive选项中定义专为该组件使用的局部指令:被存放在vm.$options['directives']中。

2.何时生效

当虚拟DOM渲染更新的时候会触发create、update、destory这三个钩子函数,从而就会执行updateDirectives函数来处理指令的相关逻辑,执行指令函数,让指令生效。

3.如何生效

updateDirectives函数就是对比新旧两份VNode上的指令列表,通过对比的异同点从而执行指令不同的钩子函数,让指令生效。

九.内置组件:keep-alive

1.使用

<keep-alive>组件可接收三个属性:

  • include - 字符串或正则表达式:只有名称匹配的组件会被缓存。
  • exclude - 字符串或正则表达式:任何名称匹配的组件都不会被缓存。
  • max - 数字:最多可以缓存多少组件实例。

2.实现原理

<keep-alive>是一个函数式组件。

1)props接收3个参数:include、exclude、max

2)在 created 钩子函数里定义并初始化了两个属性: this.cache(一个对象,用来存储需要缓存的组件) 和 this.keys(一个数组,用来存储每个需要缓存的组件的key,对应this.cache对象中的键值)。

3)当组件被销毁时,此时会调用destroyed钩子函数,在该钩子函数里会遍历this.cache对象,然后将那些被缓存的并且当前没有处于被渲染状态的组件都销毁掉并将其从this.cache对象中剔除。

4)在mounted钩子函数中观测 include 和 exclude 的变化,如果include或exclude发生了变化,即表示定义需要缓存的组件的规则或者不需要缓存的组件的规则发生了变化,那么就执行pruneCache函数。在该函数内对this.cache对象进行遍历,取出每一项的name值,用其与新的缓存规则进行匹配,如果匹配不上,则表示在新的缓存规则下该组件已经不需要被缓存,则调用pruneCacheEntry函数将这个已经不需要缓存的组件实例先销毁掉,然后再将其从this.cache对象中剔除。

5)render:由于我们也是在 标签内部写 DOM,所以可以先获取到它的默认插槽,然后再获取到它的第一个子节点。 只处理第一个子元素,所以一般和它搭配使用的有 component 动态组件或者是 router-view。

为什么要删除第一个缓存组件并且为什么命中缓存了还要调整组件key的顺序?

应用了一个缓存淘汰策略LRU:

  • 将新数据从尾部插入到this.keys中;
  • 每当缓存命中(即缓存数据被访问),则将数据移到this.keys的尾部;
  • 当this.keys满的时候,将头部的数据丢弃;

LRU的核心思想是如果数据最近被访问过,那么将来被访问的几率也更高,所以我们将命中缓存的组件key重新插入到this.keys的尾部,这样一来,this.keys中越往头部的数据即将来被访问几率越低,所以当缓存数量达到最大值时,我们就删除将来被访问几率最低的数据,即this.keys中第一个缓存的组件。这也就之前加粗强调的已缓存组件中最久没有被访问的实例会被销毁掉的原因所在。


欢迎大家批评指正

此外有部分内容感谢某小哥哥的指点及帮助

附上小哥哥的掘金地址: juejin.cn/user/303430…