Vue.js源码剖析-响应式原理

本文详细介绍了Vue.js的源码目录结构,包括编译、调试和不同构建版本的准备。通过分析入口文件,揭示了Vue实例的初始化过程,包括设置静态和实例成员、数据响应式原理。同时,探讨了首次渲染过程中数据变化如何触发视图更新,涉及watcher对象的分类和执行顺序。最后,文章提供了Vue中$nextTick的使用和实现原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


到目前为止 Vue 3.0 的正式版还没有发布新版本发布后,现有项目不会升级到 3.0,2.x 还有很长的一段过渡期
3.0 项目地址:https://github.com/vuejs/vue-next

一、源码目录结构

src
├─compiler 编译相关
├─core Vue核心库
├─platforms 平台相关代码
├─server SSR,服务端渲染
├─sfc .vue 文件编译为 js 对象
└─shared 公共的代码
了解 Flow
官网:https://flow.org/
JavaScript 的静态类型检查器
Flow 的静态类型检查错误是通过 静态类型推断 实现的
文件开头通过 // @flow 或者 /* @flow */ 声明

/* @flow */functionsquare(n: number): number {
	returnn*n;
}
square("2"); // Error!12345

二、准备工作-调试

1、打包

  • 打包工具 Rollup
    • Vue.js 源码的打包工具使用的是 Rollup,比 Webpack 轻量
    • Webpack 把所有文件当做模块,Rollup 只处理 js 文件更适合在 Vue.js 这样的库中使用
    • Rollup 打包不会生成冗余的代码
  • 安装依赖 npm i
  • 设置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 重新打包所有文件
  • https://cn.vuejs.org/v2/guide/installation.html#对不同构建版本的解释
  • dist\README.md
    在这里插入图片描述
    术语
  • 完整版:同时包含编译器(3000多行代码)和运行时的版本。
  • 编译器:用来将模板字符串编译成为 JavaScript 渲染函数的代码,体积大、效率低。
  • 运行时:用来创建 Vue 实例、渲染并处理虚拟 DOM 等的代码,体积小、效率高。基本上就是除去编译器的代码。
  • UMD https://github.com/umdjs/umd:UMD 版本通用的模块版本,支持多种模块方式。vue.js默认文件就是运行时 + 编译器的UMD 版本
  • CommonJS(cjs) http://wiki.commonjs.org/wiki/Modules/1.1:CommonJS 版本用来配合老的打包工具比如Browserify http://browserify.org/或webpack 1 https://webpack.github.io/。
  • ES Module http://exploringjs.com/es6/ch_modules.html:从 2.6 开始 Vue 会提供两个 ES Modules (ESM) 构建文件,为现代打包工具提供的版本。
  • ESM 格式被设计为可以被静态分析,所以打包工具可以利用这一点来进行“tree-shaking”并将用不到的代码排除出最终的包。
  • http://es6.ruanyifeng.com/#docs/module-loader#ES6-模块与-CommonJS-模块的差异

基于vueCLI创建的项目,默认导入的vue就是运行时版本,并且是ESM也就是es6的模块化方式

// Compiler
// 需要编译器,
把 template 转换成 render 函数
// const vm = new Vue({
//   el: '#app',
//   template: '<h1>{{ msg }}</h1>',
//   data: {
//     msg: 'Hello Vue'
//   }
// })
// Runtime
// 不需要编译器
const vm = newVue({
	el: '#app',
	render (h) {
		return h('h1', this.msg)  
	},
	data: {
		msg: 'Hello Vue'  
	}
})
- 推荐使用运行时版本,因为运行时版本相比完整版体积要小大约 30%
- 基于 Vue-CLI 创建的项目默认使用的是vue.runtime.esm.js
	- 通过查看 webpack 的配置文件
vue inspect > output.js
- 注意:*.vue文件中的模板是在构建时预编译的,最终打包后的结果不需要编译器,只需要运行时版本即可

vue inspect这个命令可以查看webpack的配置
可以输出到一个文件中来查看 vue inspect > output.js
推荐使用运行时版本
开发项目时会有很多单文件组件,这些单文件组件浏览器是不支持的,所以打包时会把单文件组件转换成js对象。在转换成js对象的过程中还会把这些模板转换成render函数,所以单文件组件运行时也是不需要编译器的

编译器的作用是把template换成render函数
运行时是不需要编译器的。比完整版体积小,执行效率高。推荐使用运行时版本

四、寻找入口文件

  • 查看 dist/vue.js 的构建过程
    执行构建
npm run dev
# "dev": "rollup -w -c scripts/config.js --sourcemap --environmentTARGET:web-full-dev"
# --environment TARGET:web-full-dev 设置环境变量 TARGET
  • script/config.js的执行过程
    • 作用:生成 rollup 构建的配置文件
    • 使用环境变量 TARGET = web-full-dev
// 判断环境变量是否有 TARGET
// 如果有的话使用 genConfig() 生成 rollup 配置文件
if (process.env.TARGET) {
module.exports=genConfig(process.env.TARGET)
} else {
// 否则获取全部配置
exports.getBuild=genConfig
exports.getAllBuilds= () =>Object.keys(builds).map(genConfig)
}
  • genConfig(name)
    • 根据环境变量 TARGET 获取配置信息
    • builds[name] 获取生成配置的信息
// 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  
},
  • resolve()
    • 获取入口和出口文件的绝对路径
const aliases=require('./alias')
const resolve=p=> {
	// 根据路径中的前半部分去alias中找别名
	const 	base=p.split('/')[0]
	if (aliases[base]) {
		return path.resolve(aliases[base], 			p.slice(base.length+1))  
	} else {
		returnpath.resolve(__dirname, '../', p)  
	}
}

结果

  • 把 src/platforms/web/entry-runtime-with-compiler.js 构建成 dist/vue.js,如果设置 --sourcemap 会生成 vue.js.map
  • src/platform 文件夹下是 Vue 可以构建成不同平台下使用的库,目前有 weex 和 web,还有服务器端渲染的库

五、从入口开始

  • src/platform/web/entry-runtime-with-compiler.js
    通过查看源码解决下面问题
  • 观察以下代码,通过阅读源码,回答在页面上输出的结果
const vm = new Vue({
	el: '#app',
	template: '<h3>Hello template</h3>',
	render (h) {
		return h('h4', 'Hello render')  
	}
})
  • 阅读源码记录
    • el 不能是 body 或者 html 标签
    • 如果没有 render,把 template 转换成 render 函数
    • 如果有 render 方法,直接调用 mount 挂载 DOM
// 1. 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 elementsinstead.`    
	)
	return this  
}
const options=this.$options
if (!options.render) {
	// 2. 把 template/el 转换成 render 函数
	......  
}
// 3. 调用 mount 方法,挂载 DOM
return mount.call(this, el, hydrating)
  • 调试代码
    • 调试的方法
const vm = new Vue({
	el: '#app',
	template: '<h3>Hello template</h3>',
	render (h) {
		return h('h4', 'Hello render')  
	}
})

在这里插入图片描述
Vue 的构造函数在哪?
Vue 实例的成员/Vue 的静态成员从哪里来的?
Vue 的构造函数在哪里

  • src/platform/web/entry-runtime-with-compiler.js 中引用了 ‘./runtime/index’
  • src/platform/web/runtime/index.js
    • 设置 Vue.config
    • 设置平台相关的指令和组件
      • 指令 v-model、v-show
      • 组件 transition、transition-group
    • 设置平台相关的__patch__方法(打补丁方法,对比新旧的 VNode)
    • 设置 $mount 方法,挂载 DOM
// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
// install platform patch function
Vue.prototype.__patch__=inBrowser?patch : noop
// public mount method
Vue.prototype.$mount=function (
	el?: string|Element,hydrating?: boolean
): Component {
	el=el&&inBrowser?query(el) : undefined
	return mountComponent(this, el, hydrating)
}
  • src/platform/web/runtime/index.js 中引用了 ‘core/index’
  • src/core/index.js
    • 定义了 Vue 的静态方法
    • initGlobalAPI(Vue)
  • src/core/index.js 中引用了 './instance/index
  • 'src/core/instance/index.js
    • 定义了 Vue 的构造函数
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')  
	}
	// 调用 _init() 方法
	this._init(options)
}
// 注册 vm 的 _init() 方法,初始化 vm
initMixin(Vue)
// 注册 vm 的$data/$props/$set/$delete/$watch
stateMixin(Vue)
// 初始化事件相关方法
// $on/$once/$off/$emit
eventsMixin(Vue)
// 初始化生命周期相关的混入方法
// _update/$forceUpdate/$destroy
lifecycleMixin(Vue)
// 混入 render
// $nextTick/_render
renderMixin(Vue)

四个导出 Vue 的模块

  • src/platforms/web/entry-runtime-with-compiler.js :核心增加了编译的功能
    • web 平台相关的入口
    • 重写了平台相关的 $mount() 方法:
      • 让$mount这个方法内部可以去编译模板,也就是把template模板转换成render函数
    • 注册了 Vue.compile() 方法,传递一个 HTML 字符串返回 render 函数
  • src/platforms/web/runtime/index.js
    • web 平台相关
    • 注册和平台相关的全局指令:v-model、v-show
    • 注册和平台相关的全局组件: v-transition、v-transition-group
    • 全局方法:
    • patch:把虚拟 DOM 转换成真实 DOM
    • $mount:挂载方法 入口中是重写这边是定义
  • src/core/index.js
    • 与平台无关
    • 设置了 Vue 的静态方法,initGlobalAPI(Vue)
  • src/core/instance/index.js 与实例相关
    • 与平台无关
    • 定义了构造函数,调用了 this._init(options) 方法
    • 给 Vue 中混入了常用的实例成员
      在这里插入图片描述

六、Vue初始化

1、两个问题

a. settings.json中
//设置不检查js语法问题,方式flow报错
“javascript.validate.enable”: false
b.使用flow泛型后代码不高亮问题:安装vscode babel javascript插件 方可高亮显示.但是超链接的功能丢失,暂时无法解决

2、静态成员

src/core/global-api/index.js

  • 初始化 Vue 的静态方法
// 注册 Vue 的静态属性/方法
initGlobalAPI(Vue)
// src/core/global-api/index.js
// 初始化 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.
// 这些工具方法不视作全局API的一部分,除非你已经意识到某些风险,否则不要去依赖他们
Vue.util= {
	warn,
	extend,
	mergeOptions,
	defineReactive
}
// 静态方法 set/delete/nextTick
Vue.set=set
Vue.delete=del
Vue.nextTick=nextTick
// 2.6 explicit observable API
// 让一个对象可响应
Vue.observable=<T>(obj: T): T=> {
observe(obj)
return obj
}
// 初始化 Vue.options 对象,并给其扩展
// components/directives/filters/_base
Vue.options=Object.create(null)
ASSET_TYPES.forEach(type=> {
	Vue.options[type+'s'] =Object.create(null)
})
// this is used to identify the "base" constructor to extend all plain-object
// components with in Weex's multi-instance scenarios.
Vue.options._base=Vue
// 设置 keep-alive 组件
extend(Vue.options.components, builtInComponents)
// 注册 Vue.use() 用来注册插件
initUse(Vue)
// 注册 Vue.mixin() 实现混入
initMixin(Vue)
// 注册 Vue.extend() 基于传入的 options 返回一个组件的构造函数
initExtend(Vue)
// 注册 Vue.directive()、 Vue.component()、Vue.filter()
initAssetRegisters(Vue)

3、实例成员

src/core/instance/index.js

  • 定义 Vue 的构造函数
  • 初始化 Vue 的实例成员
// 此处不用 class 的原因是因为方便,后续给 Vue 实例混入实例成员
function Vue (options) {
if (process.env.NODE_ENV!=='production'&&!	(thisinstanceofVue) 
) {
	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/$forceUpdate/$destroy
lifecycleMixin(Vue)
// 混入 render
// $nextTick/_render
renderMixin(Vue)

4、实例成员-init

  • initMixin(Vue)
    • 初始化 _init() 方法

5、实例成员-initState

6、调试

七、首次渲染过程

  • Vue 初始化完毕,开始真正的执行
  • 调用 new Vue() 之前,已经初始化完毕
  • 通过调试代码,记录首次渲染过程
    在这里插入图片描述
    数据响应式原理
    通过查看源码解决下面问题
  • vm.msg = { count: 0 },重新给属性赋值,是否是响应式的?
  • vm.arr[0] = 4,给数组元素赋值,视图是否会更新
  • vm.arr.length = 0,修改数组的 length,视图是否会更新
  • vm.arr.push(4),视图是否会更新
    响应式处理的入口
    整个响应式处理的过程是比较复杂的,下面我们先从
  • src\core\instance\init.js
    • initState(vm) vm 状态的初始化
    • 初始化了_data、_props、methods 等
  • src\core\instance\state.js
// 数据的初始化
if (opts.data) {
	initData(vm)
} else {
	observe(vm._data= {}, true/* asRootData */)
}
  • initData(vm) vm 数据的初始化
function initData (vm: Component) {
	let data=vm.$options.data
	// 初始化 _data,组件中 data 是函数,调用函数返回结果
	// 否则直接返回 data
	data=vm._data=typeof data==='function'?getData(data, vm)    : data|| {}
	......
	// proxy data on instance
	// 获取 data 中的所有属性
	const keys=Object.keys(data)
	// 获取 props / methods
	const props=vm.$options.props
	const methods=vm.$options.methods
	let i=keys.length
	// 判断 data 上的成员是否和  props/methods 重名......
	// observe data
	// 数据的响应式处理
	observe(data, true/* asRootData */)
}
  • src\core\observer\index.js
    • observe(value, asRootData)
    • 负责为每一个 Object 类型的 value 创建一个 observer 实例
export function observe (value: any, asRootData: ?boolean): Observer|void{
	// 判断 value 是否是对象
	if (!isObject(value) ||value instanceof VNode) {
		return  
	}
	let ob: Observer|void
	// 如果 value 有 __ob__(observer对象) 属性结束
	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  
	) {
	// 创建一个 Observer 对象
		ob=newObserver(value)  
	}
	if (asRootData&&ob) {o
		b.vmCount++  
	}
	returnob
}

数组的响应式:把会改变数组元素的这些方法给它进行重新的修补,当这些方法被调用的时候,我们调用dep的notify方法。还有个方法observeArray,遍历数组中的所有元素,把这些对象的元素转换成响应式的对象

watcher分为3种,Computed Watcher、用户Watcher(侦听器)、渲染watcher
渲染Watcher的创建时机:./src/core/instance/lifecycle.js
当数据放生变化后我们调用dep的notify方法去通知watcher,先把watcher放到一个队列里面。然后遍历这个队列调用这个队列所有watcher中的run方法。run方法中调用了渲染watcher的uodateComponent函数

渲染 wacher 创建的位置 lifecycle.js 的 mountComponent 函数中
Wacher 的构造函数初始化,处理 expOrFn (渲染 watcher 和侦听器处理不同)
调用 this.get() ,它里面调用 pushTarget() 然后 this.getter.call(vm, vm) (对于渲染 wacher 调 用 updateComponent),如果是用户 wacher 会获取属性的值(触发get操作) 当数据更新的时候,dep 中调用 notify() 方法,notify() 中调用 wacher 的 update() 方法
update() 中调用 queueWatcher()
queueWatcher() 是一个核心方法,去除重复操作,调用 flushSchedulerQueue() 刷新队列并执行 watcher
flushSchedulerQueue() 中对 wacher 排序,遍历所有 wacher ,如果有 before,触发生命周期 的钩子函数 beforeUpdate,执行 wacher.run(),它内部调用 this.get(),然后调用 this.cb() (渲染 wacher 的 cb 是 noop)
整个流程结束

vm. d e l e t e 功 能 删 除 对 象 的 属 性 。 如 果 对 象 是 响 应 式 的 , 确 保 删 除 能 触 发 更 新 视 图 。 这 个 方 法 主 要 用 于 避 开 V u e 不 能 检 测 到 属 性 被 删 除 的 限 制 , 但 是 你 应 该 很 少 会 使 用 它 。 注 意 : 目 标 对 象 不 能 是 一 个 V u e 实 例 或 V u e 实 例 的 根 数 据 对 象 。 例 : v m . delete 功能 删除对象的属性。如果对象是响应式的,确保删除能触发更新视图。这个方法主要用于避开 Vue 不能检测到属性被删除的限制,但是你应该很少会使用它。 注意: 目标对象不能是一个 Vue 实例或 Vue 实例的根数据对象。 例:vm. deleteVue使:VueVuevm.delete(vm.obj,‘msg’)

vm. w a t c h v m . watch vm. watchvm.watch( expOrFn, callback, [options] )
功能

观察 Vue 实例变化的一个表达式或计算属性函数。回调函数得到的参数为新值和旧值。表达式只 接受监督的键路径。对于更复杂的表达式,用一个函数取代。
参数
expOrFn:要监视的 $data 中的属性,可以是表达式或函数
callback:数据变化后执行的函数
函数:回调函数
对象:具有 handler 属性(字符串或者函数),如果该属性为字符串则 methods 中相应 的定义
options:可选的选项
deep:布尔类型,深度监听
immediate:布尔类型,是否立即执行一次回调函数 示例

      const vm = new Vue({
        el: '#app',
        data: {
          a: '1',
          b: '2',
          msg: 'Hello Vue',
          user: {
            firstName: '诸葛',
            lastName: '亮'
          }
        }
      })
      // expOrFn 是表达式
      vm.$watch('msg', function (newVal, oldVal) {
        console.log(newVal, oldVal)
      })
      vm.$watch('user.firstName', function (newVal, oldVal) {
        console.log(newVal)
      })
      // expOrFn 是函数 vm.$watch(function () {
      return this.a + this.b
    }, function (newVal, oldVal) {
      console.log(newVal)
    })
    // deep 是 true,消耗性能
    vm.$watch('user', function (newVal, oldVal) {
      // 此时的 newVal 是 user 对象
      console.log(newVal === vm.user)
    }, {
      deep: true // 深度监听
    })
// immediate 是 true
vm.$watch('msg', function (newVal, oldVal) {
      console.log(newVal)
    }, {
      immediate: true // 可以立即执行
    })

三种类型的 Watcher 对象
没有静态方法,因为 w a t c h 方 法 中 要 使 用 V u e 的 实 例 W a t c h e r 分 三 种 : 计 算 属 性 W a t c h e r 、 用 户 W a t c h e r ( 侦 听 器 ) 、 渲 染 W a t c h e r 创 建 顺 序 : 计 算 属 性 W a t c h e r 、 用 户 W a t c h e r ( 侦 听 器 ) 、 渲 染 W a t c h e r v m . watch 方法中要使用 Vue 的实例 Watcher 分三种:计算属性 Watcher、用户 Watcher (侦听器)、渲染 Watcher 创建顺序:计算属性 Watcher、用户 Watcher (侦听器)、渲染 Watcher vm. watch使VueWatcher:WatcherWatcher()Watcher:WatcherWatcher()Watchervm.watch()
src\core\instance\state.js
源码

Vue.prototype.$watch = function (expOrFn: string | Function, cb: any,
      options?: Object
    ): Function {
      // 获取 Vue 实例 this
      const vm: Component = this if (isPlainObject(cb)) {
        // 判断如果 cb 是对象执行 createWatcher
        return createWatcher(vm, expOrFn, cb, options)
      }
      options = options || {}
      // 标记为用户 watcher
      options.user = true
      // 创建用户 watcher 对象
      const watcher = new Watcher(vm, expOrFn, cb, options) // 判断 immediate 如果为 true
      if (options.immediate) {
        // 立即执行一次 cb 回调,并且把当前值传入 try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    // 返回取消监听的方法
    return function unwatchFn () {
      watcher.teardown()
    }
  }

查看渲染 watcher 的执行过程
当数据更新,defineReactive 的 set 方法中调用 dep.notify()
调用 watcher 的 update()
调用 queueWatcher(),把 wacher 存入队列,如果已经存入,不重复添加

  • 循环调用 flushSchedulerQueue()
    • 通过 nextTick(),在消息循环结束之前时候调用 flushSchedulerQueue() 调用
  • wacher.run()
    • 调用 wacher.get() 获取最新值
    • 如果是渲染 wacher 结束
    • 如果是用户 watcher,调用 this.cb()

异步更新队列-nextTick()

  • Vue 更新 DOM 是异步执行的,批量的
    • 在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更
      新后的 DOM。
  • vm.$nextTick(function () { /* 操作 DOM */ }) / Vue.nextTick(function () {})

vm.$nextTick() 代码演示

<div id="app">
  <p ref="p1">{{ msg }}</p>
</div>
<script src="../../dist/vue.js"></script> <script>
  const vm = new Vue({
    el: '#app',
    data: {
msg: 'Hello nextTick', name: 'Vue.js',
title: 'Title'
    },
    mounted() {
this.msg = 'Hello World' this.name = 'Hello snabbdom' this.title = 'Vue.js'
this.$nextTick(() => { console.log(this.$refs.p1.textContent)
}) }
})
</script>

定义位置
src\core\instance\render.js

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

源码
手动调用 vm.$nextTick()
在 Watcher 的 queueWatcher 中执行 nextTick() src\core\util\next-tick.js

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值