Vue2.6 源码阅读

224 阅读6分钟

一、前言

二、相关工具

1、vue2.6版本源码 github.com/vuejs/vue/t…

2、vue2 模板树生产工具 v2.template-explorer.vuejs.org/#

三、目录结构

  • |----benchmarks 性能测试
  • |----scripts 脚本文件
  • |----scr 源码
  • |  |----compiler 模板编译相关
  • |  |----core vue2核心代码
  • |  |----platforms 平台相关
  • |  |----server 服务端渲染
  • |  |----sfc 解析单文件组件
  • |  |----shared 模块间共享属性和方法

四丶VUE实例化在源码中的核心流程

如下代码为例

<template>
  <div id="app">
    <p>{{ testString }}--- {{ testNumber }}--- {{ testApp }}</p>
  </div>
</template>

<script>

export default {
  name: 'App',
  data(){
    return {
      testArray: [{a: 'a'}, 'a'],
      testString: 'string',
      testNumber: 123
    }
  },
  watch:{
    testString(newVal, oldVal){
      console.log(newVal, oldVal)
    }
  },
  computed:{
    testApp(){
      return this.testString+this.testNumber
    }
  },
  methods: {
    changeTestString(){
      this.testString = 'testString'
    }
  }
}
</script>

涉及到Vue的几个核心内容:模板语法,数据双向绑定,监听器,计算属性。

  beforeCreate(){
    console.log('beforeCreate data:', this.$data) // undefined
    console.log('beforeCreate watch:', this.$watch) // function()
    console.log('beforeCreate method', this.changeTestString)  // undefined
  },
  created(){
    console.log('created data:', this.$data) // obj{}
    console.log('created watch:', this.$watch) // function()
    console.log('created method', this.changeTestString) // function()
    console.log('created el:', this.$el) // undefined
  },

通过生命周期函数可以看出,在created阶段Vue已经完成了除dom节点挂载以外的初始化。

响应式数据(initData)

要实现数据的双向绑定,就要创建响应式数据,原理就是重写了$data中每项数据的gettersetter,这样就可以拦截到每次的取值或者改值的操作了,取值的时候收集依赖,改值的时候通知notify

this.$data.noGetterString = 'watcheGetterString'
console.log('mounted data:', this.$data)

可以看到,如果后续在data上面添加数据是不会创建gettersetter方法的,同样也无法用watch去监听参数的变化。对此Vue在原型中拓展了 $set, $delete方法在data中创建响应式数据以及删除响应式数据。

监听器(Watcher)

  • Vue中的Watcher主要分为3类 render watchercomputed watcherwatcher API

  •    render watcher: 负责视图更新。data的数据更新存在异步的问题,在源码中 通过一个 dep的方法将 observewatcher关联在一起

  •    computed watcher: 负责计算属性更新

  •    watcher API: 用户注册在this.$watch 中的方法

AST树和render函数

<template>
  <div id="app">
    <p>{{ testString }}--- {{ testNumber }}--- {{ testApp }}</p>
  </div>
</template>

  with(this) {
    return _c('div', {
      attrs: {
        "id": "app"
      }
    }, [_c('p', [_v(_s(testString) + "--- " + _s(testNumber) + "--- " +
      _s(testApp))])])
  }

使用with,vue实列执行到这个方法时,则会去找当前实例的属性。 而 _c, _s, _v等函数是用来将对应类型节点转换虚拟dom的,render执行后就能生成对应的虚拟dom树了。

依赖收集(发布订阅)

1.取值:在模板中取值的时候它就会进行依赖收集,执行dep.depend() , 最后会去重的watcher存在依赖的subs[] 中。去重是,如果模板中重复取了两次值,那也不会重复收集watcher

2.改值:在值发生变更的时候,就会触发dep.notify() ,会遍历执行其dep.subs中的所有watcher.update() ,最后还是会执行到watcher.get() ,那么就执行了 _update(_render()) 把变化更新到dom上了。

patch进行 diff 优化

patch文件中 主要函数为patchVnode()updateChildren() 。petch函数对虚拟dom进行比较,如果是相似节点则进入**patchVnode() 否则直接进行替换。

patchVnode() 把两个节点进行比较,进行文本更新,属性更新,子节点更新。

    if (isUndef(vnode.text)) {
      if (isDef(oldCh) && isDef(ch)) { // 子节点比较
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) { // 清空旧节点的文本,插入新节点的内容
        if (process.env.NODE_ENV !== 'production') {
          checkDuplicateKeys(ch)
        }
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) { // 清空旧节点内容
        removeVnodes(oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {// 文本替换
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      nodeOps.setTextContent(elm, vnode.text)
    }

根据上面代码可以把函数作用具体分为:

  1. 新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren()

  2. 如果新节点有子节点,而老节点没有子节点,先清空老节点的文本内容,然后为其新增子节点。

  3. 如果新节点没有子节点,而老节点有子节点,则移除该节点所有的子节点。

  4. 新老节点都没有子节点的时候,只是文本的替换。

updateChildren

函数在处理子节点会进行如下几步操作:

  1. 分别按新旧节点的子节点分为两组数组,然后在每组的头和尾安排两个指针指向头尾的节点。

  2. 对新旧两组节点的指针按顺序进行 6 种方式的比较,每次比较后会移动指针,6 种分别为:

      1. 当前节点指针处是否有节点,没有时,头指针向右移一位。
      2. 当前节点指针处是否有节点,没有时,尾指针向左移一位。
      3. 新旧节点的头指针进行对比,匹配时保持当前DOM 结构不变,将新旧节点的头指针右移一个单位
      4. 新旧节点的尾指针进行对比,匹配时保持当前DOM 结构不变,将新旧节点的尾指针左一个单位
      5. 节点的指针与节点的指针进行对比。匹配时,将节点的指针指向的节点的 DOM 元素插入到尾指针指向的节点的 DOM 元素之后。同时新节点尾指左移一个单位,旧节点头指针右移一个单位
      6. 节点的指针与节点的指针进行对比。匹配时,将节点的指针指向的节点的 DOM 元素插入到指针指向的节点的 DOM 元素之前。同时新节点头指针右移一个单位,旧节点尾指针左移一个单位
  3. 如果 2. 中的方式的比较都没有相同的节点,则按下面三个方法来更新(执行完毕后新节点头指针向右移动一位):

    1. if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
      idxInOld = isDef(newStartVnode.key)
        ? oldKeyToIdx[newStartVnode.key]
        : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
      if (isUndef(idxInOld)) { // New element
        createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
      } else {
        vnodeToMove = oldCh[idxInOld]
        if (sameVnode(vnodeToMove, newStartVnode)) {
          patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
          oldCh[idxInOld] = undefined
          canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
        } else {
          // same key but different element. treat as new element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        }
      }
      newStartVnode = newCh[++newStartIdx]
      
      1. 按旧节点的子节点的key值来生成一个hash表,每个key值对应其VNode节点,在查看当前新节点的头指针指向的VNodekey是否存在于hash表中,存在则插入到旧节点头指针所对应的元素之前并移除这个同类型的旧节点
      2. 如果没有时,就查询旧节点两个指针之间是否存在一个VNode对象与新节点头指针执行的VNode相等,相等则插入到旧节点头指针所对应的元素之前并移除这个同类型的旧节点
      // 此处移除节点的原因是为了防止在最后处理未处理的节点时,重复处理该节点
      1. 否则在旧节点头指针所对应的元素之前新创建一个元素。
  4. 当新旧节点的指针任意一对相交(超过)时,结束比较。

  5. 此时根据新旧序列谁完成的交叉判定处理新增节点还是卸载旧节点

  1. 旧节点完成的交叉,则说明可能新节点还有部分没有遍历完毕,需要进行新增
  2. 新节点完成的交叉,则说明旧节点可能还有部分没有遍历完毕,需要卸载

五、概览

图一为vue2的生命周期函数,当我们从源码的角度将其扩展一下就得出图二的流程。