Vue2 源码阅读笔记

首先我们要读的是vue2.6版本的源码,因为2.7版本和vue2已经有些区别了,我们用vue2普遍用的还是2.6的版本。

前置知识

rollup

无论是 vue 还是 react 框架都用了 rollup 作为构建工具,而不是webpack,为什么呢?

rollup vs webpack

模块化规范不同

rollup 是 ESmodule 规范下的构建工具,而 webpack 使用 CommonJS 规范。

ESmodule 更好的支持 Tree-Shaking, 因为它不需要导入整个工具,支持解构。

const utils = require('./utils') // CommonJS
import { ajax } from './utils' // ESmodule

ESmodule 是异步加载模块,可以避免加载进程的阻塞。

CommonJS 是同步加载模块,需要等待模块加载完成再继续往下执行。

因此 rollup 能构建出体积更小、速度更快的单文件,更受类库开发的青睐。

HMR和code-splitting

webpack 支持 HMR 和 code-splitting,因此更适合作为应用程序的打包构建工具

生态

webpack 的插件和 loader 众多,扩展性更强。

rollup 都用插件进行文件的处理,例如下面要说的 terser 等。

flow

Vue2 使用 flow 作为类型校验工具,因为当时2018以前 typescript 生态并不成熟,尤雨溪大大说它是资本控制的产品。而且当时 eslint 和 babel 都有对应的 flow 插件。当然,2018年以后就真香了,因此 Vue3 用 typescript 重构了。

flow 相关的配置在 .flowconfig文件中。

阅读思路

package.json

看 package.json,其中 files 告诉我们重点应该去看 src, dist 以及 types 目录

"files": [
    "src",
    "dist/*.js",
    "types/*.d.ts"
 ],

src

  • compiler 负责编译转换 AST

  • core 集成了核心能力

  • platform 分 web 端和 weex 端的能力,目前我们只关注 web 端

  • server 集成了 ssr 的能力

  • sfc 是解析 .vue 文件的过程

  • shared 提供通用的能力

scripts

package.json 当中的 scripts 具有 dev, build 等能力,我们要去看的就是从 build 开始我们的 Vue 做了什么,所以我们首先去看scripts下的build

build

// 如果dist目录不存在就创建dist
if (!fs.existsSync('dist')) {
  fs.mkdirSync('dist')
}

这里就可以看到我们最终的产物是放到dist里面的。

let builds = require('./config').getAllBuilds() // 处理所有的配置项

我们就要去看config中的配置项

config

builds对象

config当中主要关注builds对象,它包含的就是所有的参数以及参数对应的配置项,这些配置项可以去rollup官网去看。下面举一个例子。

const builds = {
  'web-runtime-cjs-dev': {
    entry: resolve('web/entry-runtime.js'), // 入口
    dest: resolve('dist/vue.runtime.common.dev.js'), // 打包产物
    format: 'cjs', // 格式
    env: 'development', // 环境
    banner // 文案
  }, ...
 }

接着去看aliasresolve函数

const aliases = require('./alias') // alias就是参数
const resolve = p => {
  const base = p.split('/')[0] // /前面的参数
  // aliases[base]就是alias.js文件中base对应的路径
  if (aliases[base]) {
    return path.resolve(aliases[base], p.slice(base.length + 1))
  } else {
    return path.resolve(__dirname, '../', p)
  }
}

这里的resolve是将/前的能力去掉,找到src下对应的能力。

以上面的web-runtime-cjs-dev为例,resolve('web/entry-runtime.js')它将前面的web去掉,在alias中找到了web: resolve('src/platforms/web'),因此确定web-runtime-cjs-dev的能力集成在src/platforms/web的目录下。

alias

alias主要就是将builds的参数与他们的能力相对应的src中的文件联系起来。

注意这里的resolve函数是path.resolve

// 每个模块对应的入口文件
module.exports = {
  vue: resolve('src/platforms/web/entry-runtime-with-compiler'),
  compiler: resolve('src/compiler'),
  core: resolve('src/core'),
  shared: resolve('src/shared'),
  web: resolve('src/platforms/web'),
  weex: resolve('src/platforms/weex'),
  server: resolve('src/server'),
  sfc: resolve('src/sfc')
}

genconfig

接下去是一个genconfig函数,它是提供给build文件中的buildEntry用的,这部分我们只关注生成config的能力,忽略环境和weex的内容。

function genConfig (name) {
  const opts = builds[name] // 根据传入的name获取builds中对应的配置对象
  const config = {
    input: opts.entry, // 入口
    external: opts.external, // 需要排除的外部依赖
    plugins: [
      flow(), // 用flow做类型保护
      alias(Object.assign({}, aliases, opts.alias)) // 收集所有的参数
    ].concat(opts.plugins || []),
    // 出口
    output: {
      file: opts.dest, // 目标文件
      format: opts.format, // 格式化类型
      banner: opts.banner, // 文案
      name: opts.moduleName || 'Vue'
    },
    onwarn: (msg, warn) => {
      if (!/Circular/.test(msg)) {
        warn(msg)
      }
    }
  }

  Object.defineProperty(config, '_name', {
    enumerable: false,
    value: name
  })

  return config // 这样就生成了方便rollup去解析的config对象
}

这块也建议去跟着rollup官网的配置项走一遍。

getAllBuilds

最后就是对builds每个对象进行genconfig处理并返回,注意返回的是数组。

if (process.env.TARGET) {
  module.exports = genConfig(process.env.TARGET)
} else {
  exports.getBuild = genConfig
  exports.getAllBuilds = () => Object.keys(builds).map(genConfig) // 对builds中所有的配置项进行处理
}

过滤builds数组

回到build当中,接下来根据npm run build接受的参数对config返回的builds进行过滤,还是只关注 web 端。

if (process.argv[2]) {
  const filters = process.argv[2].split(',') // 多个参数用逗号隔开
  // 从builds中去找有参数对应的出口文件的
  // 也就是说过滤出参数对应的配置
  builds = builds.filter(b => {
    return filters.some(f => b.output.file.indexOf(f) > -1 || b._name.indexOf(f) > -1)
  })
}

执行rollup

接着执行build,递归的使用next函数调用buildEntry

为什么这样设计呢?因为rollup构建完成返回的是一个Promise

build(builds)

// 这里是rollup的执行过程
function build (builds) {
  let built = 0
  const total = builds.length
  // 递归地去调用构建函数
  const next = () => {
    buildEntry(builds[built]).then(() => {
      built++
      if (built < total) {
        next()
      }
    }).catch(logError)
  }

  next()
}

buildEntry

这里的功能就是使用rollup去打包构建,其中isProd判断是否是生产环境,并使用terser进行压缩。

function buildEntry (config) {
  const output = config.output
  const { file, banner } = output // 解构出打包产物和文案
  const isProd = /(min|prod)\.js$/.test(file) // 判断生产环境
  return rollup.rollup(config) // 返回一个Promise
    .then(bundle => bundle.generate(output))
    .then(({ output: [{ code }] }) => {
      // 生产环境需要用terser进行代码压缩
      if (isProd) {
        const minified = (banner ? banner + '\n' : '') + terser.minify(code, {
          toplevel: true,
          output: {
            ascii_only: true
          },
          compress: {
            pure_funcs: ['makeMap'] // 丢弃纯函数
          }
        }).code
        return write(file, minified, true)
      } else {
        return write(file, code)
      }
    })
}

其中,toplevel的官网示例如下,判断顶层变量是否进行转换

var code = {
  "file1.js": "function add(first, second) { return first + second; }",
  "file2.js": "console.log(add(1 + 2, 3 + 4));"
};
var options = { toplevel: true };
var result = await minify(code, options);
console.log(result.code);
// console.log(3+7);

ascii_only: true转义字符串和正则表达式中的Unicode字符(影响带有非ascii字符的指令无效)

write

最后要去写文件,通过调用gzip来对代码进行压缩,然后通过fs去写文件,最终告诉你文件的大小。

function write (dest, code, zip) {
  return new Promise((resolve, reject) => {
    function report (extra) {
      console.log(blue(path.relative(process.cwd(), dest)) + ' ' + getSize(code) + (extra || ''))
      resolve()
    }
    // 通过fs写文件
    fs.writeFile(dest, code, err => {
      if (err) return reject(err)
      // 用gzip压缩
      if (zip) {
        zlib.gzip(code, (err, zipped) => {
          if (err) return reject(err)
          report(' (gzipped: ' + getSize(zipped) + ')')
        })
      } else {
        report()
      }
    })
  })
}

// 压缩后的大小
function getSize (code) {
  return (code.length / 1024).toFixed(2) + 'kb'
}

入口配置文件

build当中我们提到我们获取到了参数对应的入口配置文件,我们接下来去看入口配置文件的内容。

在 Vue2 中,我们分成 runtime 和 runtime+compiler 两种模式,区别在于 runtime 需要额外的 loader 对 .vue 文件进行处理,而 runtime+compiler 可以直接编译,因此我们去看runtime+compiler。

根据config中的builds对象,我们可以找到入口文件是位于src/platform/web目录下的entry-runtime-with-compiler.js

entry-runtime-with-compiler.js

可以看到很多针对 Vue 原型上的操作,所以我们首先要找到 Vue 的入口,也就是 Vue 是在哪创建的。

// entry-runtime-with-compiler.js
import Vue from './runtime/index' // 去找Vue的入口
// runtime/index.js
import Vue from 'core/index' // 还不是,继续找
// core/index.js
import Vue from './instance/index' // 还不是,继续找

最终在core/instance/index中找到了 Vue 的入口文件。

那么为什么不在一个文件夹里,能通过core/*映射出来呢,原因是我们的 .flowconfig 中提供了映射:

module.name_mapper='^core/\(.*\)$' -> '<PROJECT_ROOT>/src/core/\1'

core/index 

然后我们顺着我们找到 Vue 入口的这条路,倒回去看每一步都做了什么。

core/index主要做的就是初始化API,以及创建 ssr 环境。

initGlobalAPI(Vue)

// 创建ssr环境
Object.defineProperty(Vue.prototype, '$isServer', {
  get: isServerRendering
})

Object.defineProperty(Vue.prototype, '$ssrContext', {
  get () {
    /* istanbul ignore next */
    return this.$vnode && this.$vnode.ssrContext
  }
})

// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
  value: FunctionalRenderContext
})

global-api

可以参考官网全局API,我们同样忽略开发环境相关内容

export function initGlobalAPI (Vue: GlobalAPI) {
  // 在vue.config中修改的是这里的对象
  Object.defineProperty(Vue, 'config', configDef)

  // exposed util methods.
  // NOTE: these are not considered part of the public API - avoid relying on
  // them unless you are aware of the risk.
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }
  // 这里可以看到我们的$set和$nextTick都是在这里初始化的
  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick

  // 2.6 新增 observable API
  Vue.observable = <T>(obj: T): T => {
    observe(obj)
    return obj
  }

  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null) // 就是component、directive和filter
  })

  Vue.options._base = Vue

  extend(Vue.options.components, builtInComponents)

  initUse(Vue)
  initMixin(Vue)
  initExtend(Vue)
  initAssetRegisters(Vue)
}

其中,ASSET_TYPES指的是:

var ASSET_TYPES = [
  'component',
  'directive',
  'filter'
];

Vue 入口文件

入口文件代码很短,也都是重点,我直接贴出来。

import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

// Vue的构造函数
// 为什么用函数不用class? 因为Vue2中代码是通过mixin去集成的
function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue) // 限制Vue只能用new关键字创建
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

// 所有的mixin,都是针对于Vue构造函数的操作
initMixin(Vue) // 初始化
stateMixin(Vue) // 状态
eventsMixin(Vue) // 事件
lifecycleMixin(Vue) // 生命周期
renderMixin(Vue) // 渲染

export default Vue

我们可以看到 Vue 的方法是通过 mixin 的方式引入的,不过这里的 mixin 和我们使用的 mixin 不一样。

同时这里也解释了为什么要用new Vue()来创建,因为 Vue 本质上就是一个构造函数。

那么为什么不用 class 而用 function 呢?原因就在于 Vue2 通过 mixin,在 prototype 上集成了 Vue 的能力。

那么new Vue()这一步做了什么呢?也就是去看this._init(options),此时this指向new Vue()创建出来的实例。吐槽一下 mixin 注入的方法,按ctrl+左键没法跳转,只能去找。

Vue.prototype._init

init方法做了以下操作:

  1. 通过uid标记Vue实例

  2. 进行内部组件初始化

  3. 合并options

  4. 依次进行生命周期、事件、渲染的初始化

  5. 调用生命周期 hooks 函数

  6. 执行渲染。

Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++ // 通用的id,判断是创建的第几个Vue实例

    // 防止Vue实例被监听
    vm._isVue = true
    // 合并options
    if (options && options._isComponent) {
      initInternalComponent(vm, options) // 内部组件初始化
    } else {
      // 如果是一个元素,就合并其中的options
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    // expose real self
    vm._self = vm
    // 这里就是生命周期相关操作
    // 针对Vue实例添加属性和方法
    initLifecycle(vm) 
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    // 以下就是beforeCreate和created之间的差别
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    // 执行渲染
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }

mergeOptions

递归去执行options的合并。

export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  // 如果传进来的是构造函数的形式,就去找options
  if (typeof child === 'function') {
    child = child.options
  }
  // 只合并原始options对象,而不是合并后的结果
  if (!child._base) {
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm)
    }
    if (child.mixins) {
      for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm)
      }
    }
  }

  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

init

init的过程其实就是在实例vm上添加了各种属性。

callHook

export function callHook (vm: Component, hook: string) {
  pushTarget()
  const handlers = vm.$options[hook]
  const info = `${hook} hook`
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      invokeWithErrorHandling(handlers[i], vm, null, vm, info)
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook) // 通过全局emit暴露出对应的方法
  }
  popTarget()
}

beforeCreate -> created

这两个生命周期就差了当中这三步,也就是created能通过injectprovide获取到dataprops

callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

Vue.prototype.$mount

接下去就是渲染的过程了,这个方法在entry-runtime-with-compiler.js中,我们拆分来看,他接受el参数,他可以是字符串,或是对象,通过query去查找对应的元素,并且不能将Vue挂载到html或者body上。

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }
}

query

可以看到query是通过document.querySelector方法实现的。

export function query (el: string | Element): Element {
  if (typeof el === 'string') {
    const selected = document.querySelector(el)
    if (!selected) {
      process.env.NODE_ENV !== 'production' && warn(
        'Cannot find element: ' + el
      )
      return document.createElement('div')
    }
    return selected
  } else {
    return el
  }
}

判断render

接下去是一个判断,判断options是否没有render

const options = this.$options
  // resolve template/el and convert to render function
  if (!options.render) {
    ...
  }

那么什么叫有没有render

// 没有render
new Vue({
  template: '<div>{{ helloworld }}</div>'
})
// 有render
new Vue({
  render (h) {
    return h('div', this.helloworld)
  }
})

所以我们 runtime+compiler 要处理的就是没有render,而是template的情况。

template处理

如果template是一个字符串

if (typeof template === 'string') {
  if (template.charAt(0) === '#') {
    template = idToTemplate(template)
  }
}

idToTemplate用来通过 id 获取真实的 DOM 元素。

const idToTemplate = cached(id => {
  const el = query(id)
  return el && el.innerHTML
})

如果template是一个对象,其中有nodeType属性

else if (template.nodeType) {
  // 用innerHTML的方式获取template
  template = template.innerHTML
}

如果是el

else if (el) {
  template = getOuterHTML(el) // 处理是el属性的情况
}

compiler

接下去是compiler的能力。

const { render, staticRenderFns } = compileToFunctions(template, {
  outputSourceRange: process.env.NODE_ENV !== 'production',
  shouldDecodeNewlines,
  shouldDecodeNewlinesForHref,
  delimiters: options.delimiters,
  comments: options.comments
}, this)

这块要进入到compiler的入口去看。

const { compile, compileToFunctions } = createCompiler(baseOptions)

最终在src/compiler/index.js中找到了入口,可以看到这里就是AST解析的过程。

其中,parse函数将HTML转换成AST树。

optimize函数将AST树中不需要更改的DOM部分直接转为常量,无需重复创建节点。

generate避免script标签被渲染。

export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

最后调用mount,注意是运行时态的mount,而不是他自己调用自己。

return mount.call(this, el, hydrating)

runtime

mount

运行时的mount主要去看mountComponent函数

 
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

mountComponent

这个函数很长,它接受vmel作为参数。

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el;
  ...
}

首先判断是否有render,因为这里已经是运行时了,需要检查是否需要compiler。

if (!vm.$options.render) {
  // 如果没有render,就要执行compiler的过程,将template或el转换成VNode
  vm.$options.render = createEmptyVNode
}

其次去调用beforeMount钩子

callHook(vm, 'beforeMount')

然后添加Watcher

new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)

Watcher

Watcher用的是设计模式中的观察者模式的思想,Watcher 在这里起到两个作用(后面会详细讲解)

  1. 初始化的时候会执行回调函数;

  2. 当 vm 实例中的监测的数据发生变化的时候执行回调函数;

mounted

接下去就是mountedhooks

if (vm.$vnode == null) {
  vm._isMounted = true
  callHook(vm, 'mounted')
}

beforeMount -> mounted

beforeMountmounted之间做了哪些操作呢?

  1. vm._update(vm._render(), hydrating)

  2. new Watcher()

也就是说,通过mount,我们的数据的变化可以被监听到,并且通过_render生成了vnode,通过_update更新了DOM。

_render

_render的核心是vnode = render.call(vm._renderProxy, vm.$createElement),而createElement返回了_createElement的结果所以重点要去看_createElement方法。

return _createElement(context, tag, data, children, normalizationType)

首先要了解VNode的概念

VNode

具体的含义我写在注释中了,这里只列出重点。

export default class VNode {
  tag: string | void; // 标签
  data: VNodeData | void; // VNode所包含的数据
  children: ?Array<VNode>; // 子节点
  text: string | void; // 文本内容
  elm: Node | void; // 用来直接操作DOM的节点
  context: Component | void; // 当前VNode所在的上下文环境
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void; // 组件options
  componentInstance: Component | void; // 组件实例
  parent: VNode | void; // 父节点
  raw: boolean; // 包含原生HTML

  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
  ) {
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
    this.context = context
    this.key = data && data.key
    this.componentOptions = componentOptions
    this.componentInstance = undefined
    this.parent = undefined
    this.raw = false
  }
}

_createElement

核心一共两部分,

  1. 创建VNode

vnode = new VNode(
  tag, data, children,
  undefined, undefined, context
)
  1. 返回VNode

if (Array.isArray(vnode)) {
  return vnode
} else if (isDef(vnode)) {
  if (isDef(ns)) applyNS(vnode, ns)
  if (isDef(data)) registerDeepBindings(data)
  return vnode
} else {
  return createEmptyVNode()
}

_update

这是最后一步,可以看到这里最关键的就是__patch__方法,对应的是diff算法的入口。

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this
  const prevEl = vm.$el
  const prevVnode = vm._vnode
  const restoreActiveInstance = setActiveInstance(vm)
  vm._vnode = vnode
  // 这两行就表示,如果没有对应的DOM,就去创建,如果有就去更新
  if (!prevVnode) {
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false)
  } else {
    // updates
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  restoreActiveInstance()
  if (prevEl) {
    prevEl.__vue__ = null
  }
  if (vm.$el) {
    vm.$el.__vue__ = vm
  }
  if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
    vm.$parent.$el = vm.$el
  }
}

patch

同样忽略环境和ssr,比较新旧节点的方法如下:

如果新vnode为空,那么就销毁旧的vnode。

if (isUndef(vnode)) {
  if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
  return
}

如果旧的vnode为空,就创建vnode。

let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
  isInitialPatch = true
  createElm(vnode, insertedVnodeQueue)
}

如果都不为空,就再分情况讨论:

如果是相同的节点,就进行复用

const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
  patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
}

相同节点指的是:key,标签相同

function sameVnode (a, b) {
  return (
    a.key === b.key &&
    a.asyncFactory === b.asyncFactory && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

如果是真实DOM,就根据旧节点创建VNode

if (isRealElement) {
  oldVnode = emptyNodeAt(oldVnode)
}
function emptyNodeAt (elm) {
  return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
}

找到oldVnode的虚拟DOM的父元素,将父元素中的oldElm替代为新的节点。

return function patch (oldVnode, vnode, hydrating, removeOnly) {
   else {
      const oldElm = oldVnode.elm
      const parentElm = nodeOps.parentNode(oldElm)
      createElm(
        vnode,
        insertedVnodeQueue,
        oldElm._leaveCb ? null : parentElm,
        nodeOps.nextSibling(oldElm)
      )
      if (isDef(vnode.parent)) {
        let ancestor = vnode.parent
        const patchable = isPatchable(vnode)
        while (ancestor) {
          for (let i = 0; i < cbs.destroy.length; ++i) {
            cbs.destroy[i](ancestor)
          }
          ancestor.elm = vnode.elm
          if (patchable) {
            for (let i = 0; i < cbs.create.length; ++i) {
              cbs.create[i](emptyNode, ancestor)
            }
            const insert = ancestor.data.hook.insert
            if (insert.merged) {
              for (let i = 1; i < insert.fns.length; i++) {
                insert.fns[i]()
              }
            }
          } else {
            registerRef(ancestor)
          }
          ancestor = ancestor.parent
        }
      }
      if (isDef(parentElm)) {
        removeVnodes([oldVnode], 0, 0)
      } else if (isDef(oldVnode.tag)) {
        invokeDestroyHook(oldVnode)
      }
    }
  }
  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
  return vnode.elm
}

updateChildren

patch当中的updateChildren就是我们的diff算法了,我们对同层节点进行比较,确保最大程度地复用。

首先定义了八个变量,其中四个指针,分别指向旧头,旧尾,新头,新尾。

let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]

然后循环进行以下操作:

头头比较,相同则复用,新旧头指针同时右移。

if (sameVnode(oldStartVnode, newStartVnode)) {
  patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
  oldStartVnode = oldCh[++oldStartIdx]
  newStartVnode = newCh[++newStartIdx]
}

尾尾比较,相同则复用,新旧尾指针同时左移。

if (sameVnode(oldStartVnode, newStartVnode)) {
  patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
  oldStartVnode = oldCh[++oldStartIdx]
  newStartVnode = newCh[++newStartIdx]
}

旧头和新尾比较,相同则将旧头插入新节点的尾部,旧头指针右移,新尾指针左移。

if (sameVnode(oldStartVnode, newEndVnode)) {
  patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
  canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
  oldStartVnode = oldCh[++oldStartIdx]
  newEndVnode = newCh[--newEndIdx]
}

旧尾和新头比较,相同则将旧尾插入新节点的头部,旧尾指针左移,新头指针右移。

if (sameVnode(oldEndVnode, newStartVnode)) {
  patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
  canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
  oldEndVnode = oldCh[--oldEndIdx]
  newStartVnode = newCh[++newStartIdx]
}

都没有找到,根据key去直接findIndex

else {
  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)
}

最后,如果新节点数多于旧节点数,就新增节点,如果少于,就移除多余节点

if (oldStartIdx > oldEndIdx) {
  refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
  addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
  removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}

可以看到最好的情况下,Vue2使用的双端diff算法复杂度是O(n),因为新旧节点能匹配上。

而最坏情况下,复杂度是O(n^2),因为需要findIndex

这样我们Vue从构建到运行再到编译最后进行渲染和更新的路径就完成了。

  • 18
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值