写在前面
Vue3发布已有 9 个月,相比Vue2确实做了太多优化,于是想着重新再仔细全面地研究一下Vue2源码,然后对比Vue3做个整理,方便以后复习查阅。
so,今天就从 Vue2 开始吧!
预准备
-
项目地址:github.com/vuejs/vue
-
环境需要
- 2.1. 全局安装 rollup
- 2.2. 修改 dev 脚本,
package.json
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:webfull-dev",
-
开始调试
-
3.1. 打包脚本: npm run dev
这里会看到多个版本的 vue 文件,- runtime:运行时,不包含编译器
- common:cjs 规范,用于webpack1环境
- esm:ES 模块,用于**webpack2+**环境
- umd:兼容 cjs 和 amd,用于浏览器
需要注意的是,平时我们是用vue-cli基于webpack环境,webpack借助vue-loader就提前完成了编译工作,因此不需要
vue
的编译器模块,所以使用的都是runtime.esm
版本 -
3.2. 页面使用
<script src="../../dist/vue.js"></script>
-
-
文件结构关键部分
- src
- compiler 编译器相关
- core 核心代码
- components 通用组件,如
keep-alive
- global-api 全局 api,如
$set
、$delete
- instance 构造函数等
- observer 响应式相关
- util
- vdom 虚拟 dom
- components 通用组件,如
- src
初始化流程
研究源码的第一步就是从初始化入手,这个阶段的内容大多都很抽象,我们并不知道将来有什么具体作用,所以也是最容易劝退的一个环节。可以先在脑海中留个印象,后续很多流程都会回到这里的某个方法继续深究,在这个过程中不断加深记忆。
所谓初始化,就是Vue从无到有的过程,先后经历定义 Vue 的全局属性及方法、创建实例、定义实例的属性及方法、执行数据响应式、挂载 dom
从源码探究流程
首先找到入口文件夹,/src/platforms/web/
,可以看到多个入口文件
- entry-compiler.js
- entry-runtime-with-compiler.js
- entry-runtime.js
- entry-server-basic-renderer.js
- entry-server-renderer.js
从entry-runtime-with-compiler.js
进入,可以获得最全面的内容,包括运行时和编译器两大部分
注入编译器:entry-runtime-with-compiler.js /src/platforms/web/
-
作用
- 为Vue实例上的
$mount
方法注入编译器,该编译器的作用是将 template 转成 render 函数
- 为Vue实例上的
-
核心源码
if (!options.render) { // 先经过 多根/dom和字符串情况 的处理,变成单根的字符串形式 // ... if (template) { const { render, staticRenderFns } = compileToFunctions( template, { outputSourceRange: process.env.NODE_ENV !== 'production', shouldDecodeNewlines, shouldDecodeNewlinesForHref, delimiters: options.delimiters, comments: options.comments, }, this ); options.render = render; } } return mount.call(this, el, hydrating);
template
有两种情况,字符串
或dom选择器
,但最终都会处理成字符串,如果template
是多根元素,经过compileToFunctions
处理只保留第一个节点,这也是template
必须要用单根的原因这一步的关键是获得
render
函数,后续的组件渲染和更新都会用到这个方法。通常在webpack环境中并不需要注入编译器这一步,因为webpack在编译阶段借助vue-loader将单文件中的template
转成render
函数,从而减少生产环境下Vue文件的体积和编译的时间。
注入 web 运行时: /src/platforms/web/runtime/index.js
所谓 web 运行时,其实就是注入 web 平台 特有的方法,因为还要考虑Weex,所以单独分出了这个模块
-
作用:
- 为实例定义patch方法,用于 组件更新
- 为实例上的
$mount
方法额外扩展mountComponent
方法,用于将组件渲染到浏览器
-
核心源码
Vue.prototype.__patch__ = inBrowser ? patch : noop; Vue.prototype.$mount = function (el?: string | Element, hydrating?: boolean): Component { el = el && inBrowser ? query(el) : undefined; return mountComponent(this, el, hydrating); };
为实例添加的这两个方法和 挂载 息息相关,在后续的初始化与更新流程会用到。
定义全局 API:initGlobalAPI /src/core/index.js
-
作用:初始化 Vue 的静态方法,
- Vue.util 中的方法(
mergeOptions
,defineReactive
)、 Vue.observe
、Vue.use
、Vue.mixin
、Vue.extend
、Vue.component/directive/filter
- Vue.util 中的方法(
-
核心源码
Vue.set = set; Vue.delete = del; Vue.nextTick = nextTick; initUse(Vue); // 实现Vue.use函数 initMixin(Vue); // 实现Vue.mixin函数 initExtend(Vue); // 实现Vue.extend函数 initAssetRegisters(Vue); // 注册实现Vue.component/directive/filter
这一步是注入全局方法,通常在开发项目的入口文件用的会很多,比如挂载全局组件、添加指令、使用插件等。
定义实例相关: src/core/instance/index.js
-
作用
-
- 定义Vue构造器,
-
- 为Vue实例注入
API
- 为Vue实例注入
-
-
核心源码
function Vue(options) { this._init(options); } initMixin(Vue); // 初始化 this._init 方法 // 其他实例属性和方法由下面这些方法混入 stateMixin(Vue); eventsMixin(Vue); lifecycleMixin(Vue); renderMixin(Vue);
我们熟知的实例方法基本都来自这里
-
stateMixin 定义
$data
、$props
、$set
、$delete
、$watch
-
eventsMixin 定义
$on
、emit
、off
、once
-
lifecycleMixin 定义
_update
、$forceUpdate
、$destory
-
renderMixin 定义
$nextTick
、_render
为实例注入方法,基本平时工作用都有所涉及,开发项目用的最多的实例 方法都来自这里
-
实例的初始化:this._init src/core/instance/init.js
-
作用
- 调用
mergeOptions
合并Vue.options
和new Vue
传入的参数,赋值给vm.$options
- 伴随着生命周期的执行,为实例添加属性、添加事件监听(组件通信)、初始化渲染相关、执行数据响应式等等,最后调用
$mount
将组件渲染到页面
- 调用
-
核心源码
vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor), options || {}, vm); initLifecycle(vm); // 实例属性:$parent,$root,$children,$refs initEvents(vm); // 监听_parentListeners initRender(vm); // 插槽解析,$slots,$scopeSlots, $createElement() callHook(vm, 'beforeCreate'); // 接下来都是和组件状态相关的数据操作 // inject/provide initInjections(vm); // 注入祖辈传递下来的数据 initState(vm); // 数据响应式:props,methods,data,computed,watch initProvide(vm); // 提供给后代,用来隔代传递参数 callHook(vm, 'created'); // 如果设置了 el,则自动执行$mount() if (vm.$options.el) { vm.$mount(vm.$options.el); }
我们熟知的实例方法基本都来自这里
-
initLifecycle 定义
vm.$parent
、vm.$root
、vm.$refs
、vm.$children
-
initEvents 定义
vm._events
、updateComponentListeners(vm.$listeners)
-
initRender 定义
vm._c
、vm.$createElement
-
initInjections 依次执行
resolveInject
、defineReactive
-
initState 定义
initProps
、initMethods
、initData
、initComputed
、initWatch
-
initProvide 定义
vm._provide
同样也是定义实例上的属性及方法,与上一步不同的是,上个过程定义的api大多是平时工作写业务用的,这个过程提供的方法基本都为后续的源码服务,另外
initLifecycle
中定义的属性平时写组件库的小伙伴可能很熟悉。
-
渲染:$mount 与 mountComponent /src/core/instance/lifecycle.js
$mount
在初始化时分别被扩展了编译器方法和运行时方法,核心在与运行时为其扩展了mountComponent
,这是组件挂载的入口?
-
作用:组件初始化和更新最重要的流程之一,为
updateComponent
赋值更新函数,创建Watcher -
核心源码
if (process.env.NODE_ENV !== 'production' && config.performance && mark) { // ... } else { updateComponent = () => { vm._update(vm._render(), hydrating); }; } // we set this to vm._watcher inside the watcher's constructor // since the watcher's initial patch may call $forceUpdate (e.g. inside child // component's mounted hook), which relies on vm._watcher being already defined new Watcher( vm, updateComponent, noop, { before() { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate'); } }, }, true /* isRenderWatcher */ );
在 Vue 中,所有的渲染都由 Watcher(render Watcher) 完成,包括初始化渲染和组件更新,所以这一步结束后,可以在页面上看到真实 dom 的样子。
流程梳理
- 首先初始化全局的静态方法,components、filter、directive。set、delete 等,
- 然后定义Vue 实例的方法,
- 接着执行
init
方法进行实例的初始化,伴随着生命周期的进行执行初始化属性、事件监听、数据响应式,最后调用$mount
将组件挂载到页面上
总结与思考
-
为什么
$mount
要经过扩展?为了方便跨平台开发,因为Vue2新增了Weex,所以这一步是向平台注入特有的方法
-
在
mergeOptions
中发现有个监听事件绑定的操作用于组件通信时,其通信机制是怎样的?
当前组件在mergeOptions
时有一个属性parentListener
用来存放父组件通过 props 绑定的事件,组件会通过$on
将注册到自身,在使用时直接$emit
触发即可 -
生命周期的名称及应用:
-
2.1. 分类列举
- 初始化阶段:beforeCreate、created、beforeMount、mounted
- 更新阶段:beforeUpdate、updated
- 销毁阶段:beforeDestroy、destroyed
-
2.2. 应用:
- created 时,所有数据准备就绪,适合做数据获取、赋值等数据操作
- mounted 时,$el 已生成,可以获取 dom;子组件也已挂载,可以访问它们
- updated 时,数值变化已作用于 dom,可以获取 dom 最新状态
- destroyed 时,组件实例已销毁,适合取消定时器等操作
-
数据响应式
预先准备
响应式原理
借助 Object.defineReactive,可以对一个对象的某个key
进行 get 和 set 的拦截,get 时进行 依赖收集,set 时 触发依赖
但Object.defineReactive
有两个缺陷
- 无法监听动态添加的属性
- 无法监听数组变化
三个概念 及 发布订阅模式
(ps:这里只做原理简析,下文会具体提到详细的 dep 和 watcher 互相引用 等细节问题。)
- Observer
发布者:每个对象(包含子对象)有一个 Observer 实例,内部存在一个 dep,用于管理多个Watcher,当数据改变时,通过 dep 通知 Watcher 进行更新 - Dep
发布订阅中心:内部管理多个Watcher - Watcher
订阅者:执行组件的初始化和更新方法
流程一览
官网的流程图
初始化时进行数据响应式操作和创建组件 Watcher,
- 前者为对象的每个
key
进行getter
/setter
拦截,并创建dep
, - 而组件 Watcher负责组件的渲染和更新。
组件 Watcher在创建时会执行一次组件的render
函数,从而间接触发相关key
的getter
方法,将Watcher收集到key
的dep
中,
当我们更改key
的值时会触发key
的setter
方法,通过key
的dep
通知Watcher进行更新。
从源码探究流程
还记得吗,初始化执行了_init
方法,其中有一个函数initState
,这便是数据响应式的入口
initState /src/core/instance/state.js
-
作用
- 初始化
props
、methods
、data
、computed
和watch
,并进行 响应式处理
- 初始化
-
核心源码
const opts = vm.$options; // 1.props if (opts.props) initProps(vm, opts.props); // 2.methods if (opts.methods) initMethods(vm, opts.methods); // 3.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); }
可以看到,这里响应式处理的对象是
_data
,因此_data
将来是一个响应式对象,很多Vue 组件都借助了这个特点来获取响应式内容,比如Vuex这里需要特别关注
initData
方法,其核心功能是对我们平时写的data
进行数据响应化处理function initData(vm: Component) { const keys = Object.keys(data); let i = keys.length; while (i--) { const key = keys[i]; proxy(vm, '_data', key); // 将 响应式数据 代理到 this 上面 } // 执行数据响应化 observe(data, true /* asRootData */); }
我们之所以可以直接在组件内部通过
this
使用data
中的属性,是因为这里做了一个proxy(vm, '_data', key)
的操作,proxy
并没有多复杂,只是把_data
的操作直接交给vm
处理function proxy(target: Object, sourceKey: string, key: string) { sharedPropertyDefinition.get = function proxyGetter() { return this[sourceKey][key]; }; sharedPropertyDefinition.set = function proxySetter(val) { this[sourceKey][key] = val; }; Object.defineProperty(target, key, sharedPropertyDefinition); }
观察的入口:observe /src/core/observer/index.js
-
作用
- 不重复地 为 数组 和 (除 VNode 以外的)对象 创建 Observer实例
-
源码
export function observe(value: any, asRootData: ?boolean): Observer | void { if (!isObject(value) || value instanceof VNode) { return; } let ob: Observer | void; 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; }
(ps:开发环境下会对
props
,methods
校验,避免命名冲突)这里会有个细节,根据对象是否包含
__ob__
选择是否复用 Observer ,而__ob__
是哪来的呢,其实是new Observer
时操作的
Observer /src/core/observer/index.js
-
作用
- 为 对象/数组 创建 Observer 实例,并挂载到对象的
__ob__
属性上, - 创建 dep,用于数组的响应式和
Vue.set
时使用
- 为 对象/数组 创建 Observer 实例,并挂载到对象的
-
核心源码
class Observer { constructor(value: any) { this.dep = new Dep(); // 指定ob实例 def(value, '__ob__', this); if (Array.isArray(value)) { // 覆盖原型 if (hasProto) { protoAugment(value, arrayMethods); } else { copyAugment(value, arrayMethods, arrayKeys); } // 观察数组 this.observeArray(value); } else { // 观察对象 this.walk(value); } } walk(obj: Object) { const keys = Object.keys(obj); 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]); } } }
因为数组元素无法直接被
Object.defineProperty
拦截,后面会单独处理。但数组元素可能是对象,因此需要观察里面的元素。
观察对象:defineReactive /src/core/observer/index.js
-
作用:
- 通过
Object.defineProperty
为对象的key进行拦截, - 为对象的
key
创建dep,用于key
发生变化时通知更新
- 通过
-
核心源码
function defineReactive(obj: Object, key: string, val: any, customSetter?: ?Function) { const dep = new Dep(); let childOb = observe(val); Object.defineProperty(obj, key, { get: function reactiveGetter() { const value = getter ? getter.call(obj) : val; if (Dep.target) { dep.depend(); if (childOb) { childOb.dep.depend(); if (Array.isArray(value)) { dependArray(value); } } } return value; }, set: function reactiveSetter(newVal) { const value = getter ? getter.call(obj) : val; /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return; } // #7981: for accessor properties without setter // ... val = newVal; childOb = observe(newVal); dep.notify(); }, }); }
getter 负责为 自己和子元素添加依赖,setter 负责两件事
- 内容有变化时通知 dep 更新 watcher
- 观察新设置的值(新设置的值可能也是个对象)
或许你会有个小疑问,执行
setter
时为什么只改形参val
呢?其实这是JavaScript 的 闭包 特性,我的理解是,闭包为当前函数提供了一个作用域,每次
setter
被触发都会从当前作用域下取出变量val
,getter
时返回这个val
,所以我们每次操作都值都是当前作用域下的val
。
观察数组:方法覆盖 /src/core/observer/array.js
-
作用
- 数组有 7 个可以改变内部元素的方法,对这 7 个方法扩展额外的功能
-
- 观察新添加的元素,实现数组内部元素数据响应式
-
- 取出数组身上的
__ob__
,让他的dep
通知watcher
更新视图,实现数组响应式
- 取出数组身上的
-
- 数组有 7 个可以改变内部元素的方法,对这 7 个方法扩展额外的功能
-
核心源码
// 获取数组原型 const arrayProto = Array.prototype; // 克隆一份 export const arrayMethods = Object.create(arrayProto); // 7个变更方法需要覆盖 const methodsToPatch = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']; /** * Intercept mutating methods and emit events */ methodsToPatch.forEach(function (method) { // cache original method // 保存原始方法 const original = arrayProto[method]; // 覆盖之 def(arrayMethods, method, function mutator(...args) { // 1.执行默认方法 const result = original.apply(this, args); // 2.变更通知 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,让它去通知更新 ob.dep.notify(); return result; }); });
还记得吗,在创建
Observer
实例时特意给对象添加了一个dep,这里可以通过dep调用notify
方法通知更新,以此实现数组的数据响应式。
Dep /src/core/observer/dep.js
-
作用:
- 每个实例管理一组 Watcher 实例,可以通知更新
- 添加Watcher 实例到自己
- 通知Watcher 实例添加或删除自己(互相添加或删除)
-
核心源码
class Dep { constructor() { this.id = uid++; this.subs = []; } // 用于和watcher建立连接 addSub(sub: Watcher) { this.subs.push(sub); } // 用于和watcher取消引用 removeSub(sub: Watcher) { remove(this.subs, sub); } // 用于添加watcher到自己 depend() { if (Dep.target) { Dep.target.addDep(this); } } // 用于通知watcher更新 notify() { // stabilize the subscriber list first const subs = this.subs.slice(); for (let i = 0, l = subs.length; i < l; i++) { subs[i].update(); } } }
Dep 和 Watcher 相互引用,互相添加是为了处理
Vue.set
,互相删除是为了处理Vue.delete
。
Watcher /src/core/observer/watcher.js
-
作用:
- 分为 render Watcher 和 user Watcher,
- user Watcher用于
watch
和computed
, - render Watcher用于组件初始化和更新,执行粒度是
render
整个组件 - 实例存在于对象观察者的dep中
- 和dep互相添加或删除
-
核心源码
class Watcher { constructor( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm; // 用于 vm.forceUpdate if (isRenderWatcher) { vm._watcher = this; } vm._watchers.push(this); // options if (options) { this.deep = !!options.deep; this.user = !!options.user; this.lazy = !!options.lazy; this.sync = !!options.sync; this.before = options.before; } else { this.deep = this.user = this.lazy = this.sync = false; } this.cb = cb; this.id = ++uid; // uid for batching this.active = true; 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() : ''; // parse expression for getter // 初始化 的时候参数2如果是一个函数,则直接赋值给getter if (typeof expOrFn === 'function') { this.getter = expOrFn; } else { this.getter = parsePath(expOrFn); if (!this.getter) { this.getter = noop; } } this.value = this.lazy ? undefined : this.get(); } // 执行更新,重新收集依赖(初始化与更新都会再次执行这里) get() { pushTarget(this); let value; const vm = this.vm; try { value = this.getter.call(vm, vm); } catch (e) { } finally { // 深度监听 if (this.deep) { traverse(value); } // 当前Watcher赋值给Dep.target,用于重新收集依赖 popTarget(); this.cleanupDeps(); } return value; } // 添加watcher到subs addDep(dep: Dep) { const id = dep.id; // 相互添加引用 if (!this.newDepIds.has(id)) { // watcher添加dep this.newDepIds.add(id); this.newDeps.push(dep); if (!this.depIds.has(id)) { // dep添加watcher dep.addSub(this); } } } // 清除依赖,更新和初始化并不会实际执行,因为newDepIds中没有内容 cleanupDeps() { let i = this.deps.length; while (i--) { const dep = this.deps[i]; if (!this.newDepIds.has(dep.id)) { dep.removeSub(this); } } let tmp = this.depIds; this.depIds = this.newDepIds; this.newDepIds = tmp; this.newDepIds.clear(); tmp = this.deps; this.deps = this.newDeps; this.newDeps = tmp; this.newDeps.length = 0; } // 组件更新、computed、watch update() { /* istanbul ignore else */ // computed if (this.lazy) { this.dirty = true; } else if (this.sync) { this.run(); } else { // 异步更新 watcher入队 queueWatcher(this); } } // 同步执行的watcher,async:true run() { if (this.active) { // 如果是组件级别watcher,只走下面get const value = this.get(); if ( value !== this.value || // Deep watchers and watchers on Object/Arrays should fire even // when the value is the same, because the value may // have mutated. isObject(value) || this.deep ) { // set new value const oldValue = this.value; this.value = value; if (this.user) { try { this.cb.call(this.vm, value, oldValue); } catch (e) {} } else { this.cb.call(this.vm, value, oldValue); } } } } // 不立即触发的watcher,immediate:false evaluate() { this.value = this.get(); this.dirty = false; } // 和 watcher 互相引用 depend() { let i = this.deps.length; while (i--) { this.deps[i].depend(); } } // 取消监听,和 watcher 互相删除 引用,$watch 时使用 teardown() { if (this.active) { // remove self from vm's watcher list // this is a somewhat expensive operation so we skip it // if the vm is being destroyed. if (!this.vm._isBeingDestroyed) { remove(this.vm._watchers, this); } let i = this.deps.length; while (i--) { this.deps[i].removeSub(this); } this.active = false; } } }
即使只选取了 Watcher 的核心源码,但内容依然很多,主要包括重新收集依赖和
computed
、watch
、$watch
这些过程,忽略这些情况的话,其实Watcher的核心作用只有初始化和更新。值得注意的是
get
方法,最终的渲染和更新都会走到这里,并且里面有一个popTarget
方法,这是实现Vue.set
的关键。
Vue.set
说了这么多遍Vue.set
,是不是以为实现起来会很复杂,相反核心源码只有两行,其实流程大部分都在Watcher中实现了。
function set(target: Array<any> | Object, key: any, val: any): any {
defineReactive(ob.value, key, val);
ob.dep.notify();
}
调用Vue.set
后,首先为key
创建dep
,然后取出对象身上的__ob__
通知更新,更新时来到Watcher,从update
到get
,最终先执行popTarget
,将当前的render Watcher赋值给Dep.target
,然后调用组件的render
函数,间接触发key
的getter
方法,完成收集依赖并更新视图。
流程梳理
- Vue初始化时调用
this._init
,其中initState
方法用于初始化响应式数据 - 首先将
_data
代理到实例上,方便开发者通过this
调用,然后对_data
进行响应式处理,为每个被观察的对象创建观察者实例,并添加到__ob__
属性上 - 每个key拥有一个dep,
getter
时收集依赖(watcher),setter
时通知依赖(watcher)更新 - 每个对象也拥有一个dep,用于数组更新和实现
Vue.set
- 对于数组采用方法覆盖,7 个方法在执行时扩展一个额外的操作,观察新增的元素,然后让数组的
__ob__
通知watcher进行更新
总结与思考
-
dep和watcher 的关系为什么设计成多对多?
- 首先要明白的概念是,watcher包含render Watcher和user watcher,
- 其次,一个key拥有一个dep,
- 一个key可能通过props绑定给多个组件,这就有多个render Watcher
- 如果在组件中使用了
computed
、watch
,这就又添加了多个user Watcher - 到这里,dep和watcher是一对多
- Vue2 很重要的一点是,render Watcher的更新粒度是整个组件,对于一个组件,通常有多个可以触发更新的
key
,又因为一个key有一个dep,所以这种情况下dep和watcher是多对一的关系 - 综合上面两种情况,dep和watcher被设计成多对多的关系是最合适的
-
为什么需要
Vue.set
,其使用场景需要注意什么?- 存在的意义:因为
Object.defineProperty
无法动态监听,当增加key
时需要手动设置成响应式。 - 注意:添加 key 的这个对象必须是响应式 ?,因为
Vue.set
关键的一步是取出对象身上的dep触发更新完成收集依赖,如果对象不是响应式数据就不存在dep,因此无法完成依赖收集
- 存在的意义:因为
-
综合数据响应式原理,感觉最复杂的部分在于处理 数组 和 新增 key 的情况,大量逻辑都在Watcher中,导致Watcher的源码读起来很麻烦,这也是后来Vue3着重优化的一部分。后续专门整理下Vue3的变化以作对比 ?
批量异步更新
上个模块着重整理了Watcher,他负责组件的初始化渲染和更新,更新阶段为了更高效率地工作,Vue采用了批量异步更新策略
首先考虑为何要批量呢?数据响应式让我们可以通过更改数据触发视图自动更新,但如果一个函数中多次更改了数据呢,多次触发更新会很损耗性能,所以Vue将更新设置成了“批量”,即在一次同步任务中的所有更改,只会触发一次更新,既然更新和同步任务划分了界限,那么更新自然而然就被放到了异步中处理。
回顾Watcher的update
函数源码
// 组件更新 computed watch
update() {
/* istanbul ignore else */
// computed
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
// 异步更新 watcher入队
queueWatcher(this);
}
}
所以入口是queueWatcher
方法
预先准备
-
浏览器中的 事件循环模型
-
Tick 是一个微任务单元
从源码探究流程
更新的入口:queueWatcher /src/core/observer/scheduler.js
-
作用
- 不重复地向queue中添加watcher
- 一次更新流程中只调用一次
nextTick(flushSchedulerQueue)
-
核心源码
function queueWatcher(watcher: Watcher) { const id = watcher.id; if (has[id] == null) { has[id] = true; if (!flushing) { queue.push(watcher); } else { // if already flushing, splice the watcher based on its id // if already past its id, it will be run next immediately. // ... } // queue the flush if (!waiting) { waiting = true; nextTick(flushSchedulerQueue); } } }
flushSchedulerQueue
用于批量执行queue
中的任务,用waiting
加锁的意义在于,nextTick
可能会开启异步任务,因此只尝试开启一次。flushSchedulerQueue
更新结束后会重置waiting
为false
,用于下一次更新使用。
管理队列:nextTick /src/core/util/next-tick.js
-
作用
- 将
flushSchedulerQueue
添加到callbacks
- 尝试开启(一次同步任务只开启一次) 异步任务
- 将
-
核心源码
function nextTick(cb, ctx) { // 存入callbacks数组 callbacks.push(function () { // 错误处理 if (cb) { try { cb.call(ctx); } catch (e) { handleError(e, ctx, 'nextTick'); } } }); if (!pending) { pending = true; // 启动异步任务 timerFunc(); } }
timerFunc
是基于平台的真正的异步函数,在初始化时定义,一旦调用会直接在真正的任务栈中添加异步任务,所以用pending
加锁的意义是为了保证只添加一个异步任务。或许你也会疑问,上一步不是加锁了吗,这里两个状态表示的意义不同,上面
waiting
表示已经添加任务后,进入等待阶段,后面再有watdher要更新只往队列加,但是不能再尝试向队列加执行任务了,除非用户触发vm.$nextTick
;而这里的pending
表示异步更新即将执行,请不要催促。所以不可以用一个哦~
真正的异步函数:timerFunc /src/core/util/next-tick.js
- 作用
- 这是基于平台的,真正执行的异步任务,根据浏览器兼容性选择支持的异步函数
- 核心源码
这里会根据浏览器的兼容性选择最适合的异步任务,优先级为:promise > MutationObserver > setImmediate(虽然是宏任务,但优于 setTimeout) > setTimeoutif (typeof Promise !== 'undefined' && isNative(Promise)) { const p = Promise.resolve(); timerFunc = () => { p.then(flushCallbacks); // In problematic UIWebViews, Promise.then doesn't completely break, but // it can get stuck in a weird state where callbacks are pushed into the // microtask queue but the queue isn't being flushed, until the browser // needs to do some other work, e.g. handle a timer. Therefore we can // "force" the microtask queue to be flushed by adding an empty timer. if (isIOS) setTimeout(noop); }; isUsingMicroTask = true; } // else if...
流程梳理
在一次同步任务中,当执行setter
时,会取出dep执行notify
从而触发queueWatcher
,
queueWatcher
不重复地向queue
中添加watcher,然后加锁执行nextTick
,
nextTick
会向微任务队列中添加一个flushCallbacks
(即flushSchedulerQueue
)。
在 js 任务栈 中的情况大致如下:
setter
一旦被触发,微任务队列就推入 方法flushCallbacks
,整个过程只存在一个- 若当前同步任务没有结束,如果用户执行
vm.$nextTick
,只会向callbacks
中加任务,不会再产生新的flushCallbacks
- 如果用户手动执行了微任务,则向浏览器的微任务队列中推入一个微任务,在
flushCallbacks
后面 - 若同步任务执行完毕,浏览器自动从微任务队列中取出
flushCallbacks
和 用户产生的微任务 一次性执行
思考与总结
-
queue
、callbacks
、flushCallbacks
、flushSchedulerQueue
的关系flushCallbacks
,是真正执行的的异步任务,作用是刷新callbacks
callbacks
中存放的是flushSchedulerQueueflushSchedulerQueue
:刷新queue
的函数queue
中存放的是watcher
他们之间是这样的关系:
callbacks = [flushSchedulerQueue: () => while queue:[watcher1,watcher2,watcher3], $nextTick]
-
善用
$nextTick
在代码中的位置
如果我们的业务需要在更新时先获取到旧的 dom 内容,然后再进行新的 dom 操作。或许这时候可以不滥用data
,毕竟响应式数据有开销嘛,可以在 修改 data 之前执行$nextTick
,like this,receiveNotice() { this.$nextTick(() => { console.log(this.refs.map) // 更新之前的dom }) this.updateMap(); // 该方法触发dom更新 console.log(this.refs.map) // 最新的dom }
组件化原理
组件有两种声明方式
- 局部:在组件的
components
中声明即可局部使用 - 全局:通过
Vue.component
注册
预先准备
方法 Vue.component
的来源
初始化 定义全局 API 时调用了initAssetRegisters
,该方法用于注册三个全局方法:Vue.component
、Vue.filter
、Vue.directive
其中Vue.component
的核心代码:
Vue[type] = function (id, definition) {
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id;
// 调用 Vue.extend 转换成 VueComponent,将来使用时 new VueComponent 即可
definition = this.options._base.extend(definition);
}
};
Vue.extend
返回的是 VueComponent,使用时通过 new VueComponent 即可获取组件实例
Vue.component
最终将自定义组件添加到Vue.options
中,Vue.options
是全局的components
,默认只有 KeepAlive、Transition、TransitionGroup 三个组件
render
函数对于 自定义组件 和 浏览器标签 有何区别?
template
到 编译后的render
变化如下
<template>
<div id="demo">
<h1>Vue组件化机制</h1>
<comp></comp>
</div>
</template>
(function anonymous() {
with (this) {
return _c(
'div',
{ attrs: { id: 'demo' } },
[_c('h1', [_v('Vue组件化机制')]), _v(' '), _c('comp')],
1
);
}
});
可以看到对于自定义组件和 host 组件都采用了同样的处理方法:即createElement(tag)
的方式,由此可见,答案在createElement
中(这里的_c
就是createElement
的柯里化处理)
_c
_v
为何物?
在初始化的instance/renderhelpers
中为实例提供了方法别名
renderList
:v-for
_v
创建文本
_s
格式化
等
然后在initRender
中给实例声明一些方法:createElement
、_c
vm._c = (...) => createElement(...)
等
从源码探究流程
上个模块VNode中已经整理,在获取虚拟 dom时最终会调用createElement
,createElement
处理两种情况
- 如果是 浏览器标签,则
new VNode(...)
- 如果时 Vue 组件,则
createComponent(...)
所以现在我们去看createComponent
即可
createComponent /src/core/vdom/create-component.js
-
作用:返回组件的 虚拟 dom
-
核心源码
export function createComponent( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string ) { let asyncFactory; if (isUndef(Ctor.cid)) { asyncFactory = Ctor; Ctor = resolveAsyncComponent(asyncFactory, baseCtor); if (Ctor === undefined) { // 异步组件的占位符 return createAsyncPlaceholder(asyncFactory, data, context, children, tag); } } // 组件身上有双向绑定,要额外声明 事件类型 和 属性名称 if (isDef(data.model)) { transformModel(Ctor.options, data); } // 分离原生事件和自定义事件 const listeners = data.on; // replace with listeners with .native modifier // so it gets processed during parent component patch. data.on = data.nativeOn; // 安装自定义组件的钩子 installComponentHooks(data); // 返回 虚拟dom const name = Ctor.options.name || tag; const vnode = new VNode( `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`, data, undefined, undefined, undefined, context, { Ctor, propsData, listeners, tag, children }, asyncFactory ); return vnode; }
还记得
_render
函数吗,用于获得虚拟 dom,初始化和更新都会调用这个方法,
_update
做了两件重要的事情- 保存一份虚拟 dom 存到
_vnode
中,下次直接取出来使用 - 调用
__patch__
,初始化执行createElm
,更新执行patchVnode
因为初始化阶段已经得到虚拟 dom了,
patchVnode
只做diff,因此组件虚拟 dom转真实 dom的关键在createElm
中 - 保存一份虚拟 dom 存到
createElm /src/core/vdom/patch.js
-
作用
-
- 如果是浏览器标签,则创建真实 dom 树
-
- 如果是自定义组件,则调用
createComponent
- 如果是自定义组件,则调用
-
- 最终都是:虚拟 dom 转真实 dom
-
-
核心源码
function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) { // 自定义组件 if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return; } // 浏览器标签 const data = vnode.data; const children = vnode.children; const tag = vnode.tag; if (isDef(tag)) { vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode); /* istanbul ignore if */ if (__WEEX__) { } else { createChildren(vnode, children, insertedVnodeQueue); if (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue); } insert(parentElm, vnode.elm, refElm); } } else if (isTrue(vnode.isComment)) { vnode.elm = nodeOps.createComment(vnode.text); insert(parentElm, vnode.elm, refElm); } else { vnode.elm = nodeOps.createTextNode(vnode.text); insert(parentElm, vnode.elm, refElm); } }
到这里发现又回到了
createComponent
,一想到之前也有遇到类似的情景:$mount
函数也有过函数覆盖的情况,于是看了一下文件路径,发现接下来要找的createComponent
在src/core/vdom/patch.js
- createComponent() - src/core/vdom/create-component.js 组件 vnode 创建
- createComponent() - src/core/vdom/patch.js 创建组件实例并挂载,vnode 转换为 dom
createComponent src/core/vdom/patch.js
-
作用:将组件的虚拟 dom转换成真实 dom
-
核心源码
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) { let i = vnode.data; if (isDef(i)) { const isReactivated = isDef(vnode.componentInstance) && i.keepAlive; // 前面安装的钩子在hook中,只有自定义组件有init函数,执行init函数后调用组件的$mount if (isDef((i = i.hook)) && isDef((i = i.init))) { i(vnode, false /* hydrating */); } // after calling the init hook, if the vnode is a child component // it should've created a child instance and mounted it. the child // component also has set the placeholder vnode's elm. // in that case we can just return the element and be done. if (isDef(vnode.componentInstance)) { initComponent(vnode, insertedVnodeQueue); insert(parentElm, vnode.elm, refElm); if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm); } return true; } } }
这里执行了钩子函数,于是我们去前面寻找安装的钩子的地方
组件的钩子函数 installComponentHooks /src/core/vdom/create-component.js
- 作用:安装钩子,注意是安装,不是执行
- 核心源码
function installComponentHooks(data: VNodeData) { const hooks = data.hook || (data.hook = {}); // 合并用户和默认的管理钩子 for (let i = 0; i < hooksToMerge.length; i++) { const key = hooksToMerge[i]; const existing = hooks[key]; const toMerge = componentVNodeHooks[key]; if (existing !== toMerge && !(existing && existing._merged)) { hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge; } } }
hooksToMerge
包含 4 个钩子:init
、prepatch
、insert
、destory
(ps:keepAlive 组件的实现原理关键在init
部分:不需要重新创建组件,放在缓存中)
const child = (vnode.componentInstance = createComponentInstanceForVnode(vnode, activeInstance));
child.$mount(hydrating ? vnode.elm : undefined, hydrating);
init
是组件的初始化,可以看到执行了$mount
方法,因此自定义组件和host 组件在渲染阶段的区别主要是,根组件执行$mount
=> patch
=> createElm
向下递归,如果遇到host 组件,直接 createElement
(web 平台的情况),
若遇到自定义组件,则调用createComponent
,最终会执行组件的 钩子 函数:init
方法
整体流程梳理
- 定义:
Vue.component
=>通过Vue.extend获取VueComponent
=>添加到Vue.options.components中
- 初始化:
vm._update(vm.render())
=>createElement
=>createComponent
=>__patch__
=>createElm
=>createComponent
=> 执行组件的钩子函数init
- 更新:递归到组件时执行组件的钩子函数
思考与总结
-
组件化的本质是什么?
对组件化的本质,我的理解是产生虚拟 dom。因为不管是浏览器标签还是自定义组件,最终都会走向render
。 -
我看到
createComponent
对事件的监听做了单独做了处理,父子组件通信时绑定的事件如何处理的?
父子组件通过事件通信时,事件的绑定和触发都发生在子组件身上。 -
我看到用于
createComponent
中,处理钩子函数时专门对KeepAlive
做了处理,其实现原理是什么?
执行init
如果发现是KeepAlive
组件,则尝试从缓存中取,并且由于钩子函数的存在,可以做很好的动效处理。 -
全局组件和局部组件在实现原理上有何区别?
初始化Vue 组件时会调用mergeOptions
,将Vue.options.components
中的 全局组件 合并到 Vue 组件 的components
属性中,以此达到全局使用的目的。 -
存在父子关系时,生命周期执行顺序?
在整理patch得到的结论:create/destory 自上而下(深度优先),mount(从下向上)
父组件 beforeCreated ->父组件 created ->父组件 beforeMounted ->子组件 beforeCreated ->子组件 created ->子组件 beforeMounted ->子组件 mounted -> 父组件 mounted。 -
为什么说尽量少地声明全局组件?
由Vue 组件化的原理可以看到,通过Vue.component
声明的全局组件会先执行Vue.extends
创建出VueComponent,然后存放在Vue.options.components
中,并且初始化创建Vue 组件时再通过mergeOptions
注入到Vue 组件的components
选项中,因此,如果全局组件过多会占用太多资源和事件,导致首屏加载不流畅或白屏时间过长的问题。 -
组件拆分粒度的问题
在Vue2中,render Watcher的更新粒度是整个组件,所以当组件拆分不合理可能会导致一个组件有大量的虚拟 dom,这时候在diff
时会变慢,
其实反观Vue1,render Watcher的更新粒度是一个节点,可以精准更新,因此不需要虚拟 dom,这是最理想化的更新。但是由于太多Watcher占用了内存而无法开发大型项目,到了Vue2被摒弃了。快速 diff和内存占用总要有所取舍,所以还得具体场景具体分析。
带给我的收获与思考
-
大量设计模式的使用
- 发布订阅模式:数据响应式
- 工厂模式:
createPatchFunction
-
闭包
- 解析组件模板: 使用了闭包作为缓存,为了重复解析
cached
:使用闭包缓存函数createPatchFunction
: 把很多更新用的函数作为闭包defineReactive
:闭包作用域内的变量val
-
方法覆盖(扩展)
数组响应式、$mount
方法跨平台 -
精巧的工具方法
诸如类型校验、代理、密闭对象、冻结对象、检查是否是原始值、extend、只执行一次的函数等等,内容太多,看来要单独整理一篇文章了。 -
微任务的妙用
异步更新策略借助的就是浏览器的事件循环,同步任务执行完毕后会刷新微任务队列。 让我想到工作中有这么一个场景,当 websocket 推送数据后,页面关联的图表会重新render
,每个图表的render
都相对耗时,同步执行会导致每次循环都等图表渲染结束才进行下一次循环,造成页面暂时的卡顿。于是我们将render
放到微任务中处理,等循环的同步任务结束后会自动执行微任务队列,实现了页面优化。 -
和react旧diff的不同 Vue2的diff与react Fiber之前的diff还是很像的,区别是Vue2的diff过程带有一点点智能,表现为会优先处理 web 场景常见的情况,即向列表头部添加元素、向列表尾部添加元素,列表的倒叙排列、升序排列
-
PS:在读源码时发现一个
initProxy
方法,里面使用了es6的proxy
,也就是现在Vue3着重优化数据响应式的方案,但该方法只在开发环境下使用了一次,莫非当时就有了proxy代替Object.defineProperty的想法啦?
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!