vue源码学习一之响应式原理及虚拟DOM

文章内容输出来源:拉勾教育大前端高薪训练营

在拉钩训练营学习已经有一段时间了,感觉这段时间的收获远比自己独自学习强的多,自己学习的时候经常会因为惰性,无法坚持,在这里有班主任时刻关注你的学习进度(感觉对我这种懒人蛮好的),最重要的是有了一个学习的计划,不用无头苍蝇一样东一点西一点,最后什么都没记住。学习都是需要方法和技巧的,在训练营会有专业的老师为你答疑解惑,同时会教你解决问题的思路及方法,让你能够触类旁通。这篇文章主要是Vue源码学习中阶段最核心的部分,初始化、响应式原理及虚拟DOM:

一、准备工作

Vue 源码的获取

  • 项目地址:https://github.com/vuejs/vue
  • Fork 一份到自己仓库,克隆到本地,可以自己写注释提交到 github
  • 为什么分析 Vue 2.6
    1. 到目前为止 Vue 3.0 的正式版还没有发布
    2. 新版本发布后,现有项目不会升级到 3.0,2.x 还有很长的
    3. Vue3.0 项目地址:https://github.com/vuejs/vue-next

项目目录

vueSource
├─ scripts  - - - - - - - -构建相关
├─ dist    - - - - - - - -构建后文件输出目录
├─ examples- - - - - - - -应用案例
├─ flow- - - - - - - - - -类型声明,flow
├─ src- - - - - - - - - - 源码相关
│  ├─ compiler- - - - - - 编译器代码的存放目录,将 template 编译为 render 函数
│  ├─ core- - - - - - - - 存放通用的,与平台无关的代码
│  ├─ platforms- - - - - -包含平台特有的相关代码,不同平台的不同构建的入口文件也在这里
│  │  ├─ web- - - - - - - web平台
│  │  └─ weex- - - - - - -混合应用
│  ├─ server- - - - - - - 包含服务端渲染(server-side rendering)的相关代码
│  ├─ sfc- - - - - - - - -包含单文件组件(.vue文件)的解析逻辑,用于vue-template-compiler包
│  └─ shared- - - - - - - 包含整个代码库通用的代码

调试

  • 打包工具 Rollup

    • Vue.js 源码的打包工具使用的是 Rollup,比 Webpack 轻量
    • Webpack 把所有文件当做模块,Rollup 只处理 js 文件更适合在 Vue.js 这样的库中使用
    • Rollup 打包不会生成冗余的代码
  • 设置 sourcemap

    • package.json 文件中的 dev 脚本中添加参数 --sourcemap
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"
  • 执行 dev
    • npm run dev 执行打包,用的是 rollup,-w 参数是监听文件的变化,文件变化自动重新打包
    • 结果:

在这里插入图片描述

  • 调试

    • examples 的示例中引入的 vue.min.js 改为 vue.js
    • 打开 Chrome 的调试工具中的 source
  • Vue 的不同构建版本

    • npm run build 重新打包所有文件
UMDCommonJSES Module (基于构建工具使用)ES Module (直接用于浏览器)
完整版vue.jsvue.common.jsvue.esm.jsvue.esm.browser.js
只包含运行时版vue.runtime.jsvue.runtime.common.jsvue.runtime.esm.js-
完整版 (生产环境)vue.min.js--vue.esm.browser.min.js
只包含运行时版 (生产环境)vue.runtime.min.js---

二、初始化分析

入口文件

我们通过npm run dev切入:

"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",

当我们执行 npm run dev 时,根据 scripts/config.js 文件中的配置:

// Runtime+compiler development build (Browser)
  'web-full-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.js'),
    format: 'umd',
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  }

入口文件为 web/entry-runtime-with-compiler.js,最终输出 dist/vue.js。

接下来我们根据入口文件开始分析:
在web/entry-runtime-with-compiler.js中:

import Vue from './runtime/index'
import { compileToFunctions } from './compiler/index'

const mount = Vue.prototype.$mount //缓存Vue上挂载的$mount
Vue.prototype.$mount = function ( //重写$mount 用于处理template
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el) //获取元素

  //el不能是body或者html
  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
  }

  const options = this.$options
  //用于将template转换为render
  //优先判断render,存在则不再处理template
  if (!options.render) { 
    ...省略代码
  }
  //调用mount渲染dom
  return mount.call(this, el, hydrating)
}

// 在 Vue 上添加一个全局API `Vue.compile` 其值为上面导入进来的 compileToFunctions
Vue.compile = compileToFunctions


在./runtime/index中:

import Vue from 'core/index'

在core/index中:

// 从 Vue 的出生文件导入 Vue
import Vue from './instance/index'

最后在./instance/index中没有在进行引入Vue,说明这里就是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'

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}
//挂载实例方法
//注册vm的——init方法初始化vm
initMixin(Vue)
//注册vm的$data/$props/$set/$delete/$watch
stateMixin(Vue)
//初始化事件相关方法
//$on/$once/$off/$emit
eventsMixin(Vue)
//初始化生命周期相关的混入方法
//_update/$forceUpdte/$destroy
lifecycleMixin(Vue)
//混入render
//$nextTick/_render 同时通过installRenderHelpers 添加一系列方法
renderMixin(Vue)

export default Vue

我们可以看到上面是./instance/index.js 文件中全部的代码,首先分别从 ./init.js、./state.js、./render.js、
./events.js、./lifecycle.js 这五个文件中导入五个方法,分别是:initMixin、stateMixin、renderMixin、
eventsMixin 以及 lifecycleMixin,然后定义了 Vue 构造函数,其中使用了安全模式来提醒你要使用 new 操作
符来调用 Vue,接着将 Vue 构造函数作为参数,分别传递给了导入进来的这五个方法,最后导出 Vue。
这五个方法分别在Vue的原型上挂载了对应的方法(见注释):

  • initMixin 挂载_init
  Vue.prototype._init = function (options?: Object) {
    // ... _init 方法的函数体,此处省略
  }
  • stateMixin 注册vm的 d a t a / data/ data/props/ s e t / set/ set/delete/$watch
  Object.defineProperty(Vue.prototype, '$data', dataDef)
  Object.defineProperty(Vue.prototype, '$props', propsDef)
  Vue.prototype.$set = set
  Vue.prototype.$delete = del
  Vue.prototype.$watch = function(){//...}
  • eventsMixin 注册vm的 o n / on/ on/once/ o f f / off/ off/emit
Vue.prototype.$on = function (): Component {}

Vue.prototype.$once = function (): Component {}

Vue.prototype.$off = function (): Component {}

Vue.prototype.$emit = function (): Component {}
  • lifecycleMixin 注册vm的_update/ f o r c e U p d t e / forceUpdte/ forceUpdte/destroy
Vue.prototype.$once = function (): Component {}

Vue.prototype.$off = function (): Component {}

Vue.prototype.$emit = function (): Component {}
  • renderMixin 注册vm的$nextTick/_render 同时通过installRenderHelpers在原型 添加一系列方法
installRenderHelpers(Vue.prototype)

Vue.prototype.$nextTick = function (fn: Function) {}

Vue.prototype._render = function (fn: Function) {}
  • installRenderHelpers 挂载一系列方法
export function installRenderHelpers (target: any) {
  target._o = markOnce
  target._n = toNumber
  target._s = toString
  target._l = renderList
  target._t = renderSlot
  target._q = looseEqual
  target._i = looseIndexOf
  target._m = renderStatic
  target._f = resolveFilter
  target._k = checkKeyCodes
  target._b = bindObjectProps
  target._v = createTextVNode
  target._e = createEmptyVNode
  target._u = resolveScopedSlots
  target._g = bindObjectListeners
}

现在./instance/index.js中做了那些事情就基本了解了,现在我
们在返回到上一级core/index中看看做了哪些事情:

// 从 Vue 的出生文件导入 Vue
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'

// 将 Vue 构造函数作为参数,传递给 initGlobalAPI 方法,该方法来自 ./global-api/index.js 文件
//挂载一系列静态方法
initGlobalAPI(Vue)

//服务端渲染相关
// 在 Vue.prototype 上添加 $isServer 属性,该属性代理了来自 core/util/env.js 文件的 isServerRendering 方法
Object.defineProperty(Vue.prototype, '$isServer', {
  get: isServerRendering
})
// 在 Vue.prototype 上添加 $ssrContext 属性
Object.defineProperty(Vue.prototype, '$ssrContext', {
  get () {
    return this.$vnode && this.$vnode.ssrContext
  }
})
Object.defineProperty(Vue, 'FunctionalRenderContext', {
  value: FunctionalRenderContext
})

// Vue.version 存储了当前 Vue 的版本号
Vue.version = '__VERSION__'

// 导出 Vue
export default Vue

通过上面的代码我们可以知道,在 Vue.prototype 上分别添加了两个只读的属性,分别是:
$isServer 和 $ssrContext。接着又在 Vue 构造函数上定义了 FunctionalRenderContext
静态属性,这些是 ssr 相关代码。最后,在 Vue 构造函数上添加了一个静态属性 version,
存储了当前 Vue 的版本号。

这里面最主要的是initGlobalAPI方法它Vue 上添加一些全局的API,这些全局API以静态属性和
方法的形式被添加到 Vue 构造函数上,我们来看看 initGlobalAPI 方法都做了什么:
src/core/global-api/index.js

export function initGlobalAPI (Vue: GlobalAPI) {
  // config
  const configDef = {}
  configDef.get = () => config
  if (process.env.NODE_ENV !== 'production') {
    configDef.set = () => {
      warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      )
    }
  }
  //挂载只读对象config
  Object.defineProperty(Vue, 'config', configDef)
  //不推荐使用
  //util 以及 util 下的四个方法都不被认为是公共API的一部分,要避免依赖他们,
  //但是你依然可以使用,只不过风险你要自己控制。
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }
 
  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick

  //可以将一个对象转换为响应式对象
  Vue.observable = <T>(obj: T): T => {
    observe(obj)
    return obj
  }
//空对象,此对象没有原型
  Vue.options = Object.create(null)
  //ASSET_TYPES = ['component','directive','filter']
  //相当于挂载额三个方法,用于存放全局指令,组件,过滤器
  //Vue.optionscomponents: Object.create(null),
  //Vue.optionsdirectives: Object.create(null),
 //	Vue.optionsfilters: Object.create(null),
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

  Vue.options._base = Vue
 
  //builtInComponents导出一个对象里面有一个KeepAlive方法
  //   export default {
  //      KeepAlive
  //   }
  //注册全局组件
  extend(Vue.options.components, builtInComponents)
  //此时Vue.options的值就是下面的值
//   Vue.options = {
// 	    components: {
// 		    KeepAlive
// 	    },
// 	    directives: Object.create(null),
// 	    filters: Object.create(null),
// 	    _base: Vue
// }
  //Vue.use
  initUse(Vue)
  //Vue.mixin
  initMixin(Vue)
  //Vue.extend 
  initExtend(Vue)

//   Vue.component
//  Vue.directive
//  Vue.filter
// 上面的方法用于全局注册组件,指令和过滤器
  initAssetRegisters(Vue)
}

现在 core/index.js 文件的作用我们也大致分析完毕,我们继续返回上一级,platforms/web/runtime/index.js,
之前的两个文件都是在core文件中是与平台无关的代码,现在platform中是与平台相关的代码,现在我们开始分析
platforms/web/runtime/index.js

/* @flow */

import Vue from 'core/index'

// 覆盖默认值,安装平台特定的工具方法
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement

// 安装平台特定的指令与组件
//platformDirectives  -- model, show
//platformComponents  -- Transition TransitionGroup
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
//此时 Vue。option 
// Vue.options = {
// 	components: {
// 		KeepAlive,
// 		Transition,
// 		TransitionGroup
// 	},
// 	directives: {
// 		model,
// 		show
// 	},
// 	filters: Object.create(null),
// 	_base: Vue
// }

// 挂载__patch__
Vue.prototype.__patch__ = inBrowser ? patch : noop

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

// devtools 全局钩子
if (inBrowser) {
  ...省略
}

export default Vue

到目前为止Vue的构造函数大致做了哪些事情我们基本就分析完成了。后面我们再根据
./instance/index => core/index => ./runtime/index => web/entry-runtime-with-compiler.js的顺序
对其内部的相关方法做进一步的解释.

./instance/index

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'

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}
//注册vm的——init方法初始化vm
initMixin(Vue)
//注册vm的$data/$props/$set/$delete/$watch
stateMixin(Vue)
//初始化事件相关方法
//$on/$once/$off/$emit
eventsMixin(Vue)
//初始化生命周期相关的混入方法
//_update/$forceUpdte/$destroy
lifecycleMixin(Vue)
//混入render
//$nextTick/_render
renderMixin(Vue)

export default Vue

从上述代码可以看出当我们开始实例化Vue的时候,我们实际上是将参数传给this._init,
在最开始进行分析的时候我们知道this._init是在initMixin中初始化的,因此我们先来分析
initMixin做了什么

initMixin

Vue.prototype._init = function (options?: Object) {
    const vm: Component = this //vm Vue实例
    // a uid 唯一标识
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */ //性能测试相关
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // 表示当前实例时Vue实例
    vm._isVue = true
    //将Vue构造函数的options与传入的options进行合并
    //内部实现了Vue 选项的规范化及合并

    if (options && options._isComponent) { 
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {//渲染时的代理对象
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm

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

    /* istanbul ignore if *///性能测试相关
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }

    if (vm.$options.el) { //挂载实例
      vm.$mount(vm.$options.el)
    }
  }

我们开始对代码进行详细分析:

if (options && options._isComponent) { 
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }

这段代码实际上是对Vue 实例vm上添加了 $options,他是合并Vue构造函数的options以及实例化时传入的options,
内部实现了Vue选项的规范化及合并.此时Vue的构造函数的optins是这样的:

    Vue.options = {
    	components: {
    		KeepAlive,
    		Transition,
    		TransitionGroup
    	},
    	directives: {
    		model,
    		show
    	},
    	filters: Object.create(null),
    	_base: Vue
    }

我们继续往下执行:

if (process.env.NODE_ENV !== 'production') {
    initProxy(vm)
} else {//渲染时的代理对象
    vm._renderProxy = vm
}

这里主要时给vm添加一个_renderProxy属性,如果是开发环境我们调用了initProxy:

//判断当前环境是否支持 js 原生的 Proxy 特性的
  const hasProxy = typeof Proxy !== 'undefined' && isNative(Proxy);
// 声明 initProxy 变量
let initProxy

if (process.env.NODE_ENV !== 'production') {
  // ... 其他代码
  
  // 在这里初始化 initProxy
  initProxy = function initProxy (vm) {
    if (hasProxy) {
    // determine which proxy handler to use
      const options = vm.$options
      const handlers = options.render && options.render._withStripped
        ? getHandler
        : hasHandler
      vm._renderProxy = new Proxy(vm, handlers)
    } else {
      vm._renderProxy = vm
    }
  }
}
// 导出
export { initProxy }

这里面主要就是导出一个initProxy,当环境为非生产环境时initProxy是一个函数,生产环境则导出undefined,
非生产环境时initProxy会在vm上挂在一个_render方法,执行时判断当前环境是否支持 js 原生的 Proxy 特性,
支持则vm._renderProxy = new Proxy(vm, handlers),不支持vm._renderProxy = vm。这里就执行完了,我们继续向下执行:

initLifecycle(vm)

    vm._self = vm
    //vm声明周期相关变量初始化
    initLifecycle(vm)

这里是对vm生命周期相关变量初始化:

export function initLifecycle (vm: Component) {
  const options = vm.$options

  let parent = options.parent
  //找到当前组件的父组件,将当前组件添加到父组件的$children中
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }

  vm.$parent = parent
  vm.$root = parent ? parent.$root : vm

  vm.$children = []
  vm.$refs = {}
 //_开头表示私有成员
  vm._watcher = null
  vm._inactive = null
  vm._directInactive = false
  vm._isMounted = false
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}

在initLifecycle之后要执行的就是initEvents:

export function initEvents (vm: Component) {
  vm._events = Object.create(null)
  vm._hasHookEvent = false
  // 获取父元素上添加的事件
  const listeners = vm.$options._parentListeners
  if (listeners) {
    //注册自定义事件
    updateComponentListeners(vm, listeners)
  }
}

上述代码的作用主要就是定义了_events,_hasHookEvent实例属性,同时将父组件上
附加的事件注册到当前组件上,接下来要执行的就是initRender

export function initRender (vm: Component) {
  vm._vnode = null // the root of the child tree
  vm._staticTrees = null // v-once cached trees
  const options = vm.$options
  const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
  const renderContext = parentVnode && parentVnode.context
  //插槽相关
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject
 //将模板template编译成编译生成的render时,使用_c方法进行渲染的方法
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // normalization is always applied for the public version, used in
  // user-written render functions.
  //手写render函数进行渲染的方法,h函数 将虚拟DOM转换为真实DOM
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

  // $attrs & $listeners are exposed for easier HOC creation.
  // they need to be reactive so that HOCs using them are always updated
  const parentData = parentVnode && parentVnode.data

  //定义响应式数据
  if (process.env.NODE_ENV !== 'production') {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
      !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
    }, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
      !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
    }, true)
  } else {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
  }
}

在initrender之后执行的是:

    callHook(vm, 'beforeCreate') //钩子函数
    initInjections(vm) // 在初始化 data/props之前初始化inject
    initState(vm)
    initProvide(vm) // 在初始化 data/props之后初始化provide
    callHook(vm, 'created')//钩子函数

我们看下initInjections,initProvide中是怎么执行的:

export function initInjections (vm: Component) {
  //查看inject中的属性是否在在vm的_provided上,存在就将结果返回
  const result = resolveInject(vm.$options.inject, vm)
  if (result) {
    toggleObserving(false)
    Object.keys(result).forEach(key => {
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production') {
        defineReactive(vm, key, result[key], () => {
          warn(
            `Avoid mutating an injected value directly since the changes will be ` +
            `overwritten whenever the provided component re-renders. ` +
            `injection being mutated: "${key}"`,
            vm
          )
        })
      } else {
        defineReactive(vm, key, result[key])
      }
    })
    toggleObserving(true)
  }
}
//设置vm._provided
export function initProvide (vm: Component) {
  const provide = vm.$options.provide
  if (provide) {
    vm._provided = typeof provide === 'function'
      ? provide.call(vm)
      : provide
  }
}

上面执行的实际上就是将inject中存在于vm._provided上的属性值返回,将inject中的属性在vm上设置成响应式数据,。
下面是一个provide inject例子可以帮助我们理解:

let vm = new Vue({
      provide: {
        msg: '100' //vm._provided
      },
      el: '#app',
      components: {
        'MyButton': {
          inject: ['msg'], //inject 会被定义为响应式数据
          template: `<div>{{msg}}</div>`,
        }
      }
    })

接下来就是initState的实现:

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  //将props中的数据注入到vm上并设置为响应式数据
  if (opts.props) initProps(vm, opts.props)
  //将methods中的值注入到vm,
  //同时判断methods中的值是否是函数
  //是否在props中有重名
  //是否已$或者_开头
  if (opts.methods) initMethods(vm, opts.methods)
  //判断是否是函数,是函数就执行获得结果
  //判断data是否是对象
  //判断在methods,props中有重名
  //最后使用observe将data转换为响应式对象
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  //将计算属性与侦听器注入实例
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

initState 包括了:initProps、initMethods、initData、initComputed 以及 initWatch。
主要是用于初始化相关选项的,最后initState要执行的是:

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }

这里的 m o u n t 来 自 于 入 口 文 件 e n t r y − r u n t i m e − w i t h − c o m p i l e r . j s 开 始 我 们 也 已 经 分 析 过 , 这 里 面 主 要 是 重 写 了 mount 来自于入口文件entry-runtime-with-compiler.js开始我们也已经分析过,这里面 主要是重写了 mountentryruntimewithcompiler.jsmount,在内部添加了将template模板处理为render函数的方法,将其挂载在options.render
上,然后在执行代理的runtime/index.js中的$mount方法:

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

从上面我们可以了解到runtime/index.js中的$mount方法是使用instance/lifecircle.js中的
mountComponent方法渲染页面:

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  ...省略
  callHook(vm, 'beforeMount')//钩子

  let updateComponent
  /* istanbul ignore if */
  //定义updateComponent
  updateCompoCnent = () => {
      vm._update(vm._render(), hydrating)
    }
  //执行Watcher
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

这里面主要就是执行了两个钩子,以及顶一个一个updateCompoCnent将其作为参数传递给了Watcher对象,
并且初始执行一次。以上就是Vue初始化的大致流程。后续我们继续对Vue的响应式原理继续机型分析。

三、响应式原理分析

响应式原理过程比较复杂,我们从状态的初始化开始开始:

状态的初始化位于./instance/index.js的initMixin函数中的initState中,
他初始化了_data,_props,methods等

export function initState (vm: Component) {
  ...省略
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  ...省略
}
//\core\instance\state.js
function initData (vm: Component) {
   ...省略 相关代码用于判断data上的属性在methods或者props上已经定义
  // 响应式处理的入口
  observe(data, true /* asRootData */)
}

observer入口

这里observe就是响应式处理的入口:

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  //判断当前value是否已经存在__ob__表逝已经是响应式数据
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (//判断当前数据是否可以进行响应式处理
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    //当前数据不是响应式数据则进行响应式处理
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

这里可以看到observer中主要是对传入的数据进行判断是否已经是响应式数据,
当前数据是否可以进行响应式处理,当传入的数据符合处理的条件时,将数据传入
Observer构造函数进行处理

Observer构造函数

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data
  constructor (value: any) {
    this.value = value
    this.dep = new Dep() //这里的dep主要是用于vm.$set $delete
    this.vmCount = 0
    //给当前数据定义一个不可遍历的属性__ob__,值就是当前Observer实例
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {//判断当前环境是否支持对象的__proto__属性
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      //对数组进行响应式处理
      //数组的响应式处理只有push/pop等会改变数组自身的方法才能触发响应式
      //同时会进一步遍历数组中的每一项,如果时对象/数组仍会进行对应的响应式处理
      this.observeArray(value)
    } else {
      //处理数据
      this.walk(value)
    }
  }
  walk (obj: Object) {
    const keys = Object.keys(obj)
    //遍历value上的每一项进行响应式处理
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
  //数组的响应式处理
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

从上述代码我们可以知道在Observer中主要时对 传入的数组进行区分,
数组与对象执行不同的响应式处理,传入的数组为数组时会对传入的数组
原型上的方法(会改变数组自身的方法)进行重写这里主要设计到了
arrayMethods这个定义的常量,我们可以看到protoAugment中主要时将value的
proto__指向了传入的arrayMethods,copyAugment中因为不存在__proto,所以
使用的是definedPrototype的方法:

function protoAugment (target, src: Object) {
  target.__proto__ = src
}
function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}

我们看下arrayMethods是怎么处理的:

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

methodsToPatch.forEach(function (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
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    //对插入的数据进行响应式处理
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})

以上代码其实是缓存了Array原型,在arrayMethods上定义了push等属性,
当value需要执行push方法是实际上执行的是它__proto__也就是arrayMethods.push,
而arrayMethods.push实际上是我们自定义的方法,他中间执行了我们插入的一些代码,
最后返回的result实际上是我们最开始缓存的Array.push的执行结果。
而对象的响应式处理则设计到defineReactive这个方法:

defineReactive

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean //是否深度监听
) {
  const dep = new Dep() //依赖收集

  const property = Object.getOwnPropertyDescriptor(obj, key)
  //是否可配置
  if (property && property.configurable === false) {
    return
  }

  //缓存原有的set get
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
 // $set $delete相关,同时对传入的val进行响应式处理-深度监听
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,//可枚举
    configurable: true,//可配置
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {// Watcher
        dep.depend() //收集依赖
        if (childOb) {//用于vm.$set $delete执行时能触发更新,对象与数组处理方式不同
          childOb.dep.depend()
          //如果属性是数组 则特殊处理对象数组依赖
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      // 新值旧值是否相等 特殊值 NaN
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      //对nweVal进行响应式处理
      childOb = !shallow && observe(newVal)
      //派发更新
      dep.notify()
    }
  })
}

现在我们知道依赖收集实在defineReactive中进行定义的,它对每个属性设置的setter,getter
当触发getter时会进行依赖的收集,触发setter时会进行派发更新的操作,这里面涉及到Watcher及Dep
现在我们来分析Watcher,Dep

Watcher Dep

  • Watcher 分为三类:Computed Watcher,用户Watcher(侦听器),渲染Watcher
  • 创建顺序 执行顺序 Computed Watcher,用户Watcher(侦听器),渲染Watcher
  • 渲染Watcher \core\instance\lifecycle.js – mountComponent

conexport function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  ...省略分非生产环境提示相关代码
  callHook(vm, 'beforeMount')//钩子

  let updateComponent
  /* istanbul ignore if */
  //定义updateComponent
  updateCompoCnent = () => {
    //_render 生成虚拟DOM
    //调用了__patch__ 对比两个虚拟DOM的差别,最后将差异更新到真实DOM
      vm._update(vm._render(), hydrating)
    }
  //执行Watcher
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* 表示时渲染watcher */)
  hydrating = false
  //钩子
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

Watxher.js

export default class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    //存储了所有的Watcher 包括用户Watcher Computed Watcher 渲染Watcher
    vm._watchers.push(this)
    // 记录选项
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user //是否是用户watcher
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
   //第三个参数记录渲染Watcher之外的两个Watcher的回调
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true //是否是活动watcher
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // 第二个参数
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn) //返回用户watcher监听的属性对应的值得函数
      if (!this.getter) {
        //错误处理
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }
  //入栈将当前Watcher赋值给Dep。Target
  //父子组件嵌套时先把父组件对应的Watcher入栈
  //再去处理子组件Watcher,子组件处理完毕出栈,继续执行组组件Watcher
  // export function pushTarget (target: ?Watcher) {
  //   targetStack.push(target)
  //   Dep.target = target
  // }

  // export function popTarget () {
    //出栈
  //   targetStack.pop()
  //   Dep.target = targetStack[targetStack.length - 1]
  // }

  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      //此处进行处理时可能会包含子组件,子组件中也会存在Watcher
      //页面渲染
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      if (this.deep) {//深度监听
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
//添加依赖
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }

  /**
   * Clean up for dependency collection.
   */
  cleanupDeps () {
    //省略
  }
  //数据更新
  update () {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {//渲染Watcher
    //将Watcher加入队列执行 最后会执行Watcher.run
      queueWatcher(this)
    }
  }
  run () {
    if (this.active) {//Watcher 存活
      const value = this.get() //更新视图,渲染Watcher为undefined
      if (
        value !== this.value ||
        isObject(value) ||
        this.deep
      ) {
        //用户Watcher 侦听器会执行
        const oldValue = this.value
        this.value = value
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

  teardown () {
//省略
  }
}

Dep.js

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

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  depend () { //依赖收集
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
  //触发更新
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

根据上述代码我们基本了解了 依赖收集与更新的过程:

  1. 首次渲染时我们的执行顺序,_init => initState => initData => observer(value)
    => new Observer(value) => definedReactive,在definedReactive中对数据进行响应式处理
    同时在触发get时收集依赖,set时触发更新;
  2. 在_init最后会执行 m o u n t 进 行 首 次 渲 染 , v m . mount进行首次渲染,vm. mount,vm.mount(增加了template的处理) => vm.$mount
    => mountComponent,在mountComponent中会定义一个updateComponent函数,并将其作为第二个
    参数对Watcher进行实例化,在实例化过程中会执行watcher.get方法,get执行时会将Dep.Target
    设置为当前Watcher实例,并执行updateComponent,updateComponent内部执行了将render函数
    转换为虚拟DOM,并对比新旧差异更新到真实DOM上的操作,其中在获取data中的数据时会触发响应式
    数据中的get方法,其中会将当前Watcher收集到对应属性的Dep实例中。这样首次渲染就进本完成。
  3. 当数据发生更新时会触发set方法,会后会触发dep.notify(),这个方法是执行dep收集到的Watcher
    的update方法=> queueWatcher(this)(Watcher分三种类型,这里会对他的执行顺序进行排序) =>
    watcher.run() =>watcher.get(),后面的执行基本一致,不会重复执行依赖收集,addSub会判断是否
    已经收集过当前Watcher实例。

set函数

当我们需要给一个对象或者数组新增一个属性时,需要使用vm.$set或者Vue.set
进行设置,现在我们来看下set的源码

export function set (target: Array<any> | Object, key: any, val: any): any {
  //省略错误提示
  //数组的处理方法
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  //判断设置的苏醒是否时已存在的属性
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  //如果时Vue实例或者根数据
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  //如果不是响应式数据
  if (!ob) {
    target[key] = val
    return val
  }
  //对象新增的属性进行响应式处理
  defineReactive(ob.value, key, val)
  //派发更新
  ob.dep.notify()
  return val
}

上面就是set的源码,传入的数据必须对象,如果是数组则使用splice更新数据,这里的splice是
通过响应式处理的方法会触发dep.notify,如果是对象则判断当前设置的key是否已存在,存在则额直接
赋值,会触发set方法,不能是Vue实例或者$data根数据,通过ob判断target是否是响应式数据,不是直接赋值,
如果是则对新增对象进行响应式处理,并使用dep.notify派发更新。

nextTick

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  //将回调加入callbacks中
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    //调用
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

//timerFunc 支持Primise的情况,不支持微任务会转为宏任务调用
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
 
//flushCallbacks 调用callbacks里面的任务
function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

四、虚拟DOM相关

使用js对象描述虚拟DOM,Vue的虚拟DOM借鉴了snabbdom.js

为什么使用虚拟DOm

  • 避免直接操作DOM,提高开发效率
  • 作为中间层可以跨平台
  • 虚拟DOM不一定可以提高性能
    • 首次渲染其实性能并没有直接操作DOM好
    • 但是复杂情况下可以提社工渲染性能,当有大量DOm操作时
      虚拟DOM会对比新旧DOm差别,使用diff算法最终将差异更新到
      DOM,不会每次都操作DOM,并且可以设置key值让节点尽可能重用
      避免节点的大量重绘

实现过程

const vm = new Vue({
  el:"app",
  render(h) {
    return h('h1', {}, 'sss')
  }
})

这里面的h函数其实就是createElement:
vm.$createElement 用户传入的render函数内的h vm._c 模板编译生成的render函数内的h

  // vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
export function createElement (
 context: Component,
 tag: any,
 data: any,
 children: any,
 normalizationType: any,
 alwaysNormalize: boolean
): VNode | Array<VNode> {
 // 判断第三个参数
 // 如果 data 是数组或者原始值的话就是 children,实现类似函数重载的机制
//根据传入数据的不同判断参数如何传递
 if (Array.isArray(data) || isPrimitive(data)) {
  normalizationType = children
  children = data
  data = undefined
}
//根据结果不同用于处理children参数
 if (isTrue(alwaysNormalize)) {
  normalizationType = ALWAYS_NORMALIZE
}
 return _createElement(context, tag, data, children, normalizationType)
}

export function _createElement (
 context: Component,
 tag?: string | Class<Component> | Function | Object,
 data?: VNodeData,
 children?: any,
 normalizationType?: number
): VNode | Array<VNode> {
  //判断传入的data参数是否是响应式数据 -- 不应该传入响应式数据
 if (isDef(data) && isDef((data: any).__ob__)) {
  ……
// 返回一个空的虚拟节点
  return createEmptyVNode()
}
 // object syntax in v-bind
// <component v-bind:is="com"></component>
//判断是否是component组件
 if (isDef(data) && isDef(data.is)) {
  tag = data.is
}
 if (!tag) {
  // in case of component :is set to falsy value
  return createEmptyVNode()
}
 ……
 //作用于插槽
 if (Array.isArray(children) &&
  typeof children[0] === 'function'
) {
  data = data || {}
  data.scopedSlots = { default: children[0] }
  children.length = 0
}
 if (normalizationType === ALWAYS_NORMALIZE) {
 // 去处理 children 用户传入的rendr函数
// 当手写 render 函数的时候调用
// 判断 children 的类型,如果是原始值的话转换成 VNode 的数组
// 如果是数组的话,继续处理数组中的元素
// 如果数组中的子元素又是数组(slot template),递归处理
// 如果连续两个节点都是字符串会合并文本节点
//最终需要赶回一维数组
  children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
// 把二维数组转换为一维数组
// 如果 children 中有函数组件的话,函数组件会返回数组形式
// 这时候 children 就是一个二维数组,只需要把二维数组转换为一维数组
  children = simpleNormalizeChildren(children)
}
 let vnode, ns
// 判断 tag 是字符串还是组件
 if (typeof tag === 'string') {
  let Ctor
  ns = (context.$vnode && context.$vnode.ns) ||
config.getTagNamespace(tag)
  // 如果是浏览器的保留标签,创建对应的 VNode
if (config.isReservedTag(tag)) {
   // platform built-in elements
   vnode = new VNode(
    config.parsePlatformTagName(tag), data, children,
    undefined, undefined, context
  )
 } else if ((!data || !data.pre) && isDef(Ctor =
resolveAsset(context.$options, 'components', tag))) {
   // component
   // 否则的话创建组件的Vnode对象
vnode = createComponent(Ctor, data, context, children, tag)
 } else {
   vnode = new VNode(
    tag, data, children,
    undefined, undefined, context
  )
 }
} else {
  // direct component options / constructor
  vnode = createComponent(tag, data, context, children)
}
 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()
  }
}

以上就是vNode的创建过程,现在我们将创建好的vNode传递给vm._update 方法

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode//之前处理过的vNode对象不存在说明时首次渲染
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    //首次渲染  不存在说明时首次渲染
    if (!prevVnode) {
      //对比差异更新到真实DOM
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      //对比差异更新到真实DOM
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    restoreActiveInstance()
    // update __vue__ reference
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook.
  }

这里面主要实现的时根据_vnode判断是否存在已处理过的vNode,不存在说明是首次渲染,根据不同情况
传递不同参数给__patch__,这里面主要是把对比传入的两个DOM使用diff提取差异更新到真丝DOM:

// 浏览器环境
export const patch: Function = createPatchFunction({ 
  nodeOps,//dom相关api
  modules //平台相关模块(style...)与平台无关模块(指令、ref)的合集
  })
// createPatchFunction 返回patch函数
export function createPatchFunction (backend) {
 let i, j
 const cbs = {}
 const { modules, nodeOps } = backend
 // 把模块中的钩子函数全部设置到 cbs 中,将来统一触发
 // cbs --> { 'create': [fn1, fn2], ... }
 for (i = 0; i < hooks.length; ++i) {
  cbs[hooks[i]] = []
  for (j = 0; j < modules.length; ++j) {
   if (isDef(modules[j][hooks[i]])) {
    cbs[hooks[i]].push(modules[j][hooks[i]])
  }
 }
}
 ……
 ……
 ……
 return function patch (oldVnode, vnode, hydrating, removeOnly) {
 }
}

patch函数的执行过程

function patch (oldVnode, vnode, hydrating, removeOnly) {
// 如果没有 vnode 但是有 oldVnode,执行销毁的钩子函数
 if (isUndef(vnode)) {
  if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
  return
}
 let isInitialPatch = false
 const insertedVnodeQueue = [] //插入的vNode的数组,为了以后触发interted钩子
 if (isUndef(oldVnode)) {
// 如果没有 oldVnode,创建 vnode 对应的真实 DOM
//表示只创建vNode但是不挂载
  isInitialPatch = true
//将vNode转换为真实DOM
  createElm(vnode, insertedVnodeQueue)
} else {
  // 判断当前 oldVnode 是否是真实 DOM 元素(首次渲染)
  const isRealElement = isDef(oldVnode.nodeType)
  if (!isRealElement && sameVnode(oldVnode, vnode)) {
    // 如果不是真实 DOM,并且两个 VNode 是 sameVnode,这个时候开始执行 Diff
   patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null,removeOnly)
 } else {//如果是真实DOM 首次渲染
   if (isRealElement) {// 如果是真实DOM
    if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
     oldVnode.removeAttribute(SSR_ATTR)
     hydrating = true
   }
    …… 
    //将真实DOM转换为vNode
    oldVnode = emptyNodeAt(oldVnode)
  }
   // 真实DOM元素
   const oldElm = oldVnode.elm
   const parentElm = nodeOps.parentNode(oldElm)
   // 转换为真实DOM
   createElm(
    vnode,
    insertedVnodeQueue,
    oldElm._leaveCb ? null : parentElm,
    nodeOps.nextSibling(oldElm)
  )
   // 处理父节点占位符
   if (isDef(vnode.parent)) {
    ...
  }
   // 如果父节点存在
   if (isDef(parentElm)) {
  //删除所有旧节点并触发对应钩子
    removeVnodes(parentElm, [oldVnode], 0, 0)
  } else if (isDef(oldVnode.tag)) {
  //触发destory 钩子
    invokeDestroyHook(oldVnode)
  }
 }
}
//触发insertedVnodeQueue的inserted钩子,isInitialPatch 是否立即执行钩子
 invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
 return vnode.elm
}

patch中有两个核心函数createElm与patchVnode,现在我们来看他们的实现:

createElm主要是将vNode转换为真实DOM

function createElm (
vnode, //虚拟DOM
insertedVnodeQueue,
parentElm,//需要插入的父节点
refElm,
nested,
ownerArray,
index
) {
  //vnode.el表示是否渲染过 ownerArray是否有子节点
 if (isDef(vnode.elm) && isDef(ownerArray)) {
  vnode = ownerArray[index] = cloneVNode(vnode)
}
 vnode.isRootInsert = !nested // 组件
 if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
  return
}
 const data = vnode.data
 const children = vnode.children
 const tag = vnode.tag
 if (isDef(tag)) { //标签
  if (process.env.NODE_ENV !== 'production') {
   if (data && data.pre) {
    creatingElmInVPre++
  }
  //判断标签是否是自定义标签,是否注册了对应组件
   if (isUnknownElement(vnode, creatingElmInVPre)) {
    //警告
  }
 }
//是否有命名空间  svg
  vnode.elm = vnode.ns
   ? nodeOps.createElementNS(vnode.ns, tag)
 : nodeOps.createElement(tag, vnode)
//给dom元素设置作用域
  setScope(vnode)
  /* istanbul ignore if */
  if (__WEEX__) {
   ……
 } else {
  //将子元素转换为DOM对象
   createChildren(vnode, children, insertedVnodeQueue)
   if (isDef(data)) {
  //触发所有的create钩子函数
    invokeCreateHooks(vnode, insertedVnodeQueue)
  }
  //将创建好的DOM插入真实DOM
   insert(parentElm, vnode.elm, refElm)
 }
} else if (isTrue(vnode.isComment)) {//vNode是否是注释节点
  vnode.elm = nodeOps.createComment(vnode.text)
  insert(parentElm, vnode.elm, refElm)
} else { //文本节点
  vnode.elm = nodeOps.createTextNode(vnode.text)
  insert(parentElm, vnode.elm, refElm)
}
}

patchVnode主要是对比新旧vNode差异,然后更新到真实DOM上

function patchVnode (
  oldVnode,
  vnode,
  insertedVnodeQueue,
  ownerArray,
  index,
  removeOnly
) {
  // 如果新旧节点是完全相同的节点,直接返回
  if (oldVnode === vnode) {
   return
 }
  if (isDef(vnode.elm) && isDef(ownerArray)) {
    // clone reused vnode
   vnode = ownerArray[index] = cloneVNode(vnode)
 }
  const elm = vnode.elm = oldVnode.elm
  ……
  // 触发 prepatch 钩子函数
  let i
  const data = vnode.data
  if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
   i(oldVnode, vnode)
 }
// 获取新旧 VNode 的子节点
  const oldCh = oldVnode.children
  const ch = vnode.children
  // 触发 update 钩子函数
  if (isDef(data) && isPatchable(vnode)) {
   for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode,vnode)
   if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
 }
  // 如果 vnode 没有 text 属性(说明有可能有子元素)
  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)
   }
    // 如果新节点有子节点,并且旧节点有 text
    // 清空旧节点对应的真实 DOM 的文本内容
    if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
    // 把新节点的子节点添转换成真实 DOM,添加到 elm
    addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
  } else if (isDef(oldCh)) {
    // 如果旧节点有子节点,新节点没有子节点
    // 移除所有旧节点对应的真实 DOM
    removeVnodes(elm, oldCh, 0, oldCh.length - 1)
  } else if (isDef(oldVnode.text)) {
    // 如果旧节点有 text,新节点没有子节点和 text
    nodeOps.setTextContent(elm, '')
  }
 } else if (oldVnode.text !== vnode.text) {
   // 如果新节点有 text,并且和旧节点的 text 不同
   // 直接把新节点的 text 更新到 DOM 上
   nodeOps.setTextContent(elm, vnode.text)
 }
  // 触发 postpatch 钩子函数
  if (isDef(data)) {
   if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode,vnode)
 }
}

在上述实现中,diff算法的核心是updateChildren:

  function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    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]
    let oldKeyToIdx, idxInOld, vnodeToMove, refElm
    const canMove = !removeOnly

    if (process.env.NODE_ENV !== 'production') {
      checkDuplicateKeys(newCh)
    }

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        //oldKeyToIdx为oldNode中key与index对应对象
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)//判断老节点中是否有新节点key
        if (isUndef(idxInOld)) { // 没有说明是新节点 执行插入操作在老节点之前
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {// 有 说明key存在
          vnodeToMove = oldCh[idxInOld]//老节点需要处理的额那个子节点
          if (sameVnode(vnodeToMove, newStartVnode)) {//新旧节点选择器也相同 说明需要更新
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            oldCh[idxInOld] = undefined//将旧节点中被更新的那个节点赋值为undefined,否则下次会继续对比
            //将跟新节点插入到当前旧开始节点之前
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {//key相同但是元素不同,说明新建元素,插入
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        newStartVnode = 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)
    }
  }
  • 定义相关变量 oldStartIdx, newStartIdx, oldEndIdx, newEndIdx, oldStartVnode,
    oldEndVnode,newStartVnode,newEndVnode,oldKeyToIdx,idxInOld,elmToMove,before
  • 判断条件执行不同任务(循环当oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx)
  • 判断oldStartVnode,oldEndVnode,newStartVnode,newEndVnode是否为空,为空则索引相应处理 start ++ end –
  • 根据响应顺序对比节点
    1. oldStartVnode,newStartVnode;
    2. oldEndVnode,newEndVnode;
    3. oldStartVnode,newEndVnode
    4. oldEndVnode,newStartVnode
    5. 以上都不满足,根据key值查找;
      • 定义一个对象遍历oldCh,存放存在key的节点及索引的键值对 – {[key]:[index]}
      • 判断该对象是否存在newStartVnode.key的属性,不存在则节点为新增节点,执行插入操作,
        插入位置为oldStartVnode之前,同时newStartIdx++
      • 如果存在节点,则找到newStartVnode在oldCh中对应的节点,使用变量存储
      • 判断新老对比节点 的sel是否相同,不同说明发生了变化,执行插入节点操作,插入位置为oldStartVnode之前
      • 新老节点sel相同说明是同一个节点,使用patchVnode更新节点,将老节点定义为undefined,
        将该老节点移动至oldStartVnode之前
      • 索引++
    • 新旧节点有一个遍历完成
    1. 老节点先遍历完成:记录当前newStartIdx与newEndIdx,将多余节点插入 newEndIdx + 1 位置节点之前
    2. 新节点先遍历完成:清除节点
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值