赞
踩

分析 vue 作为一个 MVVM 框架的基本实现原理
准备知识
[].slice.call(lis): 将伪数组转换为真数组node.nodeType: 得到节点类型Object.defineProperty(obj, propName, {}): 给对象添加/修改属性(指定描述符)
configurable: true/false 是否可以重新 defineenumerable: true/false 是否可以枚举(for…in / keys())value: 指定初始值writable: true/false value 是否可以修改get: 回调函数, 用来得到当前属性值set: 回调函数, 用来监视当前属性值的变化Object.keys(obj): 得到对象自身可枚举的属性名的数组DocumentFragment: 文档碎片(高效批量更新多个节点)obj.hasOwnProperty(prop): 判断 prop 是否是 obj 自身的属性在 new Vue 初始化的时候,会对我们组件的数据 props 和 data 进行初始化
源码地址:src/core/instance/init.js - 15行
export function initMixin (Vue: Class<Component>) { // 在原型上添加 _init 方法 Vue.prototype._init = function (options?: Object) { ... vm._self = vm initLifecycle(vm) // 初始化实例的属性、数据:$parent, $children, $refs, $root, _watcher...等 initEvents(vm) // 初始化事件:$on, $off, $emit, $once initRender(vm) // 初始化渲染: render, mixin callHook(vm, 'beforeCreate') // 调用生命周期钩子函数 initInjections(vm) // 初始化 inject initState(vm) // 初始化组件数据:props, data, methods, watch, computed initProvide(vm) // 初始化 provide callHook(vm, 'created') // 调用生命周期钩子函数 ... } }
export function initState (vm: Component) { vm._watchers = [] const opts = vm.$options // 初始化 props if (opts.props) initProps(vm, opts.props) // 初始化 methods if (opts.methods) initMethods(vm, opts.methods) // 初始化 data if (opts.data) { initData(vm) } else { // 没有 data 的话就默认赋值为空对象,并监听 observe(vm._data = {}, true /* asRootData */) } // 初始化 computed if (opts.computed) initComputed(vm, opts.computed) // 初始化 watch if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) } }
initProps()、initData()、observe()
源码地址:src/core/instance/state.js - 65行
这里主要做的是:
props 列表defineReactive 设置成响应式proxy() 把属性代理到当前实例上,如把 vm._props.xx 变成 vm.xx,就可以访问function initProps (vm: Component, propsOptions: Object) { // 父组件传入子组件的 props const propsData = vm.$options.propsData || {} // 经过转换后最终的 props const props = vm._props = {} // 存放 props 的 key,就算 props 值空了,key 也会在里面 const keys = vm.$options._propKeys = [] const isRoot = !vm.$parent // 转换非根实例的 props if (!isRoot) { toggleObserving(false) } for (const key in propsOptions) { keys.push(key) // 校验 props 类型、default 属性等 const value = validateProp(key, propsOptions, propsData, vm) // 在非生产环境中 if (process.env.NODE_ENV !== 'production') { const hyphenatedKey = hyphenate(key) if (isReservedAttribute(hyphenatedKey) || config.isReservedAttr(hyphenatedKey)) { warn(`hyphenatedKey 是保留属性,不能用作组件 prop`) } // 把 props 设置成响应式的 defineReactive(props, key, value, () => { // 如果用户修改 props 发出警告 if (!isRoot && !isUpdatingChildComponent) { warn(`避免直接改变 prop`) } }) } else { // 把 props 设置为响应式 defineReactive(props, key, value) } // 把不在默认 vm 上的属性,代理到实例上 // 可以让 vm._props.xx 通过 vm.xx 访问 if (!(key in vm)) { proxy(vm, `_props`, key) } } toggleObserving(true) }
源码地址:src/core/instance/state.js - 113行
这里主要做的是:
data,并拿到 keys 集合keys 集合,来判断有没有和 props 里的属性名或者 methods 里的方法名重名的proxy() 把 data 里的每一个属性都代理到当前实例上,就可以通过 this.xx 访问了observe 监听整个 datafunction initData (vm: Component) { // 获取当前实例的 data let data = vm.$options.data // 判断 data 的类型 data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {} if (!isPlainObject(data)) { data = {} process.env.NODE_ENV !== 'production' && warn(`数据函数应该返回一个对象`) } // 获取当前实例的 data 属性名集合 const keys = Object.keys(data) // 获取当前实例的 props const props = vm.$options.props // 获取当前实例的 methods 对象 const methods = vm.$options.methods let i = keys.length while (i--) { const key = keys[i] // 非生产环境下判断 methods 里的方法是否存在于 props 中 if (process.env.NODE_ENV !== 'production') { if (methods && hasOwn(methods, key)) { warn(`Method 方法不能重复声明`) } } // 非生产环境下判断 data 里的属性是否存在于 props 中 if (props && hasOwn(props, key)) { process.env.NODE_ENV !== 'production' && warn(`属性不能重复声明`) } else if (!isReserved(key)) { // 都不重名的情况下,代理到 vm 上 // 可以让 vm._data.xx 通过 vm.xx 访问 proxy(vm, `_data`, key) } } // 监听 data observe(data, true /* asRootData */) }
源码地址:src/core/observer/index.js - 110行
这个方法主要就是用来给数据加上监听器的
这里主要做的是:
vnode 的对象类型或者不是引用类型,就直接跳出Observer 的数据添加一个 Observer,也就是监听者export function observe (value: any, asRootData: ?boolean): Observer | void { // 如果不是'object'类型 或者是 vnode 的对象类型就直接返回 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 }
源码地址:src/core/observer/index.js - 37行
这是一个类,作用是把一个正常的数据成可观测的数据
这里主要做的是:
value 打上已经是响应式属性的标记,避免重复操作defineReactive()创建响应式对象observe()对每一个元素进行监听export class Observer { value: any; dep: Dep; vmCount: number; // 根对象上的 vm 数量 constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 // 给 value 添加 __ob__ 属性,值为value 的 Observe 实例 // 表示已经变成响应式了,目的是对象遍历时就直接跳过,避免重复操作 def(value, '__ob__', this) // 类型判断 if (Array.isArray(value)) { // 判断数组是否有__proty__ if (hasProto) { // 如果有就重写数组的方法 protoAugment(value, arrayMethods) } else { // 没有就通过 def,也就是Object.defineProperty 去定义属性值 copyAugment(value, arrayMethods, arrayKeys) } this.observeArray(value) } else { this.walk(value) } } // 如果是对象类型 walk (obj: Object) { const keys = Object.keys(obj) // 遍历对象所有属性,转为响应式对象,也是动态添加 getter 和 setter,实现双向绑定 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]) } } }
源码地址:src/core/observer/index.js - 135行
这个方法的作用是定义响应式对象
这里主要做的是:
dep 实例observe,递归监听,以保证不管结构嵌套多深,都能变成响应式对象Object.defineProperty() 劫持对象属性的 getter 和 gettergetter 会调用 dep.depend() 把观察者 push 到依赖的数组 subs 里去,也就是依赖收集setter 会做以下操作
setter 属性的直接跳出observe() 递归监听dep.notify() 派发更新export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { // 创建 dep 实例 const dep = new Dep() // 拿到对象的属性描述符 const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } // 获取自定义的 getter 和 setter const getter = property && property.get const setter = property && property.set if ((!getter || setter) && arguments.length === 2) { val = obj[key] } // 如果 val 是对象的话就递归监听 // 递归调用 observe 就可以保证不管对象结构嵌套有多深,都能变成响应式对象 let childOb = !shallow && observe(val) // 截持对象属性的 getter 和 setter Object.defineProperty(obj, key, { enumerable: true, configurable: true, // 拦截 getter,当取值时会触发该函数 get: function reactiveGetter () { const value = getter ? getter.call(obj) : val // 进行依赖收集 // 初始化渲染 watcher 时访问到需要双向绑定的对象,从而触发 get 函数 if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, // 拦截 setter,当值改变时会触发该函数 set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val // 判断是否发生变化 if (newVal === value || (newVal !== newVal && value !== value)) { return } if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter() } // 没有 setter 的访问器属性 if (getter && !setter) return if (setter) { setter.call(obj, newVal) } else { val = newVal } // 如果新值是对象的话递归监听 childOb = !shallow && observe(newVal) // 派发更新 dep.notify() } }) }
上面说了通过 dep.depend 来做依赖收集,可以说 Dep 就是整个 getter 依赖收集的核心了
vm 对象来代理 data 对象中所有属性的操作Object.defineProperty()给 vm 添加与 data 对象的属性对应的属性描述符 b. 所有添加的属性都包含 getter/setter c. getter/setter 内部去操作 data 中对应的属性数据el 的所有子节点取出, 添加到一个新建的文档 fragment 对象中fragment 中的所有层次子节点递归进行编译解析处理
fragment 添加到 el 中显示/RegExp.\$1 namedata 中取出表达式对应的属性值textContentmethods 中得到对应的事件处理函数对象text/html/class msg/myClassdata 中根据表达式得到对应的值v-text—textContent 属性v-html—innerHTML 属性v-class–className 属性一旦更新了 data 中的某个属性数据, 所有界面上直接使用或间接使用了此属性的节点都会 更新
vue 中用来实现数据绑定的一种技术defineProperty()来监视 data 中所有属性(任意层次)数据的变化, 一旦变化就去更新界面a. 用来对 data 所有属性数据进行劫持的构造函数
b. 给 data 中所有属性重新定义属性描述(get/set)
c. 为 data 中的每个属性创建对应的 dep 对象
a. data 中的每个属性(所有层次)都对应一个 dep 对象
b. 创建的时机:
dep 对象data 中的某个属性值被设置为新的对象时c. 对象的结构
{
id, // 每个 dep 都有一个唯一的 id
subs //包含 n 个对应 watcher 的数组(subscribes 的简写)
}
d. subs 属性说明
watcher 被创建时, 内部将当前 watcher 对象添加到对应的 dep 对象的 subs 中data 属性的值发生改变时, subs 中所有的 watcher 都会收到更新的通知,从而最终更新对应的界面Dep源码地址:src/core/observer/dep.js
这是一个类,它实际上就是对 Watcher 的一种管理
这里首先初始化一个 subs 数组,用来存放依赖,也就是观察者,谁依赖这个数据,谁就在这个数组里,然后定义几个方法来对依赖添加、删除、通知更新等
另外它有一个静态属性 target,这是一个全局的 Watcher,也表示同一时间只能存在一个全局的 Watcher
let uid = 0 export default class Dep { static target: ?Watcher; id: number; subs: Array<Watcher>; constructor () { this.id = uid++ this.subs = [] } // 添加观察者 addSub (sub: Watcher) { this.subs.push(sub) } // 移除观察者 removeSub (sub: Watcher) { remove(this.subs, sub) } depend () { if (Dep.target) { // 调用 Watcher 的 addDep 函数 Dep.target.addDep(this) } } // 派发更新(下一章节介绍) notify () { ... } } // 同一时间只有一个观察者使用,赋值观察者 Dep.target = null const targetStack = [] export function pushTarget (target: ?Watcher) { targetStack.push(target) Dep.target = target } export function popTarget () { targetStack.pop() Dep.target = targetStack[targetStack.length - 1] }
a. 用来解析模板页面的对象的构造函数(一个实例)
b. 利用 compile 对象解析模板页面
c. 每解析一个表达式(非事件指令)都会创建一个对应的 watcher 对象, 并建立 watcher 与 dep 的关系
d. complie 与 watcher 关系: 一对多的关系
a. 模板中每个非事件指令或表达式都对应一个 watcher 对象
b. 监视当前表达式数据的变化
c. 创建的时机: 在初始化编译模板时
d. 对象的组成
{
vm, //vm 对象
exp, //对应指令的表达式
cb, //当表达式所对应的数据发生改变的回调函数
value, //表达式当前的值
depIds //表达式中各级属性所对应的 dep 对象的集合对象 //属性名为 dep 的 id, 属性值为 dep
}
源码地址:src/core/observer/watcher.js
Watcher 也是一个类,也叫观察者(订阅者),这里干的活还挺复杂的,而且还串连了渲染和编译
let uid = 0 export default class Watcher { ... constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm if (isRenderWatcher) { vm._watcher = this } vm._watchers.push(this) // Watcher 实例持有的 Dep 实例的数组 this.deps = [] this.newDeps = [] this.depIds = new Set() this.newDepIds = new Set() this.value = this.lazy ? undefined : this.get() if (typeof expOrFn === 'function') { this.getter = expOrFn } else { this.getter = parsePath(expOrFn) } } get () // 该函数用于缓存 Watcher // 因为在组件含有嵌套组件的情况下,需要恢复父组件的 Watcher pushTarget(this) let value const vm = this.vm try { // 调用回调函数,也就是upcateComponent,对需要双向绑定的对象求值,从而触发依赖收集 value = this.getter.call(vm, vm) } catch (e) { ... } finally { // 深度监听 if (this.deep) { traverse(value) } // 恢复Watcher 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)) { // 把当前 Watcher push 进数组 dep.addSub(this) } } } // 清理不需要的依赖(下面有) cleanupDeps () { ... } // 派发更新时调用(下面有) update () { ... } // 执行 watcher 的回调 run () { ... } depend () { let i = this.deps.length while (i--) { this.deps[i].depend() } } }
a. data 中的一个属性对应一个 dep, 一个 dep 中可能包含多个 watcher(模板中有几个表达式使用到了同一个属性)
b. 模板中一个非事件表达式对应一个 watcher, 一个 watcher 中可能包含多个 dep(表达式是多层: a.b)
c. 数据绑定使用到 2 个核心技术
defineProperty()消息订阅与发布
v-model 指令时, 给当前元素添加 input 监听value 发生改变时, 将最新的值赋值给当前表达式所对应的 data 属性在首次渲染挂载的时候,还会有这样一段逻辑
mountComponent 源码地址:src/core/instance/lifecycle.js - 141行
export function mountComponent (...): Component { // 调用生命周期钩子函数 callHook(vm, 'beforeMount') let updateComponent updateComponent = () => { // 调用 _update 对 render 返回的虚拟 DOM 进行 patch(也就是 Diff )到真实DOM,这里是首次渲染 vm._update(vm._render(), hydrating) } // 为当前组件实例设置观察者,监控 updateComponent 函数得到的数据,下面有介绍 new Watcher(vm, updateComponent, noop, { // 当触发更新的时候,会在更新之前调用 before () { // 判断 DOM 是否是挂载状态,就是说首次渲染和卸载的时候不会执行 if (vm._isMounted && !vm._isDestroyed) { // 调用生命周期钩子函数 callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) // 没有老的 vnode,说明是首次渲染 if (vm.$vnode == null) { vm._isMounted = true // 调用生命周期钩子函数 callHook(vm, 'mounted') } return vm }
依赖收集:
watcher ,进入 watcher 构造函数里就会执行 this.get() 方法pushTarget(this),就是把 Dep.target 赋值为当前渲染 watcher 并压入栈(为了恢复用)this.getter.call(vm, vm),也就是上面的 updateComponent() 函数,里面就执行了 vm._update(vm._render(), hydrating)vm._render() 就会生成渲染 vnode,这个过程中会访问 vm 上的数据,就触发了数据对象的 gettergetter 都有一个 dep,在触发 getter 的时候就会调用 dep.depend() 方法,也就会执行 Dep.target.addDep(this)push 到 subs 里,到这就已经完成了依赖的收集,不过到这里还没执行完,如果是对象还会递归对象触发所有子项的getter,还要恢复 Dep.target 状态移除订阅就是调用 cleanupDeps() 方法。比如在模板中有 v-if 我们收集了符合条件的模板 a 里的依赖。当条件改变时,模板 b 显示出来,模板 a 隐藏。这时就需要移除 a 的依赖
这里主要做的是:
deps,移除 dep.subs 数组中的 Watcher 的订阅newDepIds 和 depIds 交换,newDeps 和 deps 交换newDepIds 和 newDeps 清空// 清理不需要的依赖 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 }
触发 setter 的时候会调用 dep.notify() 通知所有订阅者进行派发更新
notify () {
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// 如果不是异步,需要排序以确保正确触发
subs.sort((a, b) => a.id - b.id)
}
// 遍历所有 watcher 实例数组
for (let i = 0, l = subs.length; i < l; i++) {
// 触发更新
subs[i].update()
}
}
触发更新时调用
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
// 组件数据更新会走这里
queueWatcher(this)
}
}
源码地址:src/core/observer/scheduler.js - 164行
这是一个队列,也是 Vue 在做派发更新时的一个优化点。就是说在每次数据改变的时候不会都触发 watcher 回调,而是把这些 watcher 都添加到一个队列里,然后在 nextTick 后才执行
这里和下一小节 flushSchedulerQueue() 的逻辑有交叉的地方,所以要联合起来理解
主要做的是:
has 对象查找 id,保证同一个 watcher 只会 push 一次else 如果在执行 watcher 期间又有新的 watcher 插入进来就会到这里,然后从后往前找,找到第一个待插入的 id 比当前队列中的 id 大的位置,插入到队列中,这样队列的长度就发生了变化waiting 保证 nextTick 只会调用一次export function queueWatcher (watcher: Watcher) { // 获得 watcher 的 id const id = watcher.id // 判断当前 id 的 watcher 有没有被 push 过 if (has[id] == null) { has[id] = true if (!flushing) { // 最开始会进入这里 queue.push(watcher) } else { // 在执行下面 flushSchedulerQueue 的时候,如果有新派发的更新会进入这里,插入新的 watcher,下面有介绍 let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) } // 最开始会进入这里 if (!waiting) { waiting = true if (process.env.NODE_ENV !== 'production' && !config.async) { flushSchedulerQueue() return } // 因为每次派发更新都会引起渲染,所以把所有 watcher 都放到 nextTick 里调用 nextTick(flushSchedulerQueue) } } }
源码地址:src/core/observer/scheduler.js - 71行
这里主要做的是:
watcher.run()。需要注意的是,遍历的时候每次都会对队列长度进行求值,因为在 run 之后,很可能又会有新的 watcher 添加进来,这时就会再次执行到上面的 queueWatcherfunction flushSchedulerQueue () { currentFlushTimestamp = getNow() flushing = true let watcher, id // 根据 id 排序,有如下条件 // 1.组件更新需要按从父到子的顺序,因为创建过程中也是先父后子 // 2.组件内我们自己写的 watcher 优先于渲染 watcher // 3.如果某组件在父组件的 watcher 运行期间销毁了,就跳过这个 watcher queue.sort((a, b) => a.id - b.id) // 不要缓存队列长度,因为遍历过程中可能队列的长度发生变化 for (index = 0; index < queue.length; index++) { watcher = queue[index] if (watcher.before) { // 执行 beforeUpdate 生命周期钩子函数 watcher.before() } id = watcher.id has[id] = null // 执行组件内我们自己写的 watch 的回调函数并渲染组件 watcher.run() // 检查并停止循环更新,比如在 watcher 的过程中又重新给对象赋值了,就会进入无限循环 if (process.env.NODE_ENV !== 'production' && has[id] != null) { circular[id] = (circular[id] || 0) + 1 if (circular[id] > MAX_UPDATE_COUNT) { warn(`无限循环了`) break } } } // 重置状态之前,先保留一份队列备份 const activatedQueue = activatedChildren.slice() const updatedQueue = queue.slice() resetSchedulerState() // 调用组件激活的钩子 activated callActivatedHooks(activatedQueue) // 调用组件更新的钩子 updated callUpdatedHooks(updatedQueue) }
终于可以更新了,updated 大家都熟悉了,就是生命周期钩子函数
上面调用 callUpdatedHooks() 的时候就会进入这里, 执行 updated 了
function callUpdatedHooks (queue) {
let i = queue.length
while (i--) {
const watcher = queue[i]
const vm = watcher.vm
if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'updated')
}
}
}
至此 Vue2 的响应式原理流程的源码基本就分析完毕了,接下来就介绍一下上面流程中的不足之处
使用 Object.defineProperty 实现响应式对象,还是有一些问题的
setter 的给对象添加新的响应式属性时,可以使用一个全局的 API,就是 Vue.set() 方法
源码地址:src/core/observer/index.js - 201行
set 方法接收三个参数:
这里主要做的是:
splice 替换__ob__,说明不是一个响应式对象,直接赋值返回export function set (target: Array<any> | Object, key: any, val: any): any { if (process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target)) ) { warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`) } // 如果是数组 而且 是合法的下标 if (Array.isArray(target) && isValidArrayIndex(key)) { target.length = Math.max(target.length, key) // 直接使用 splice 就替换,注意这里的 splice 不是原生的,所以才可以监测到,具体看下面 target.splice(key, 1, val) return val } // 到这说明是对象 // 如果 key 存在于 target 里,就直接赋值,也是可以监测到的 if (key in target && !(key in Object.prototype)) { target[key] = val return val } // 获取 target.__ob__ const ob = (target: any).__ob__ 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 } // 在 Observer 里介绍过,如果没有这个属性,就说明不是一个响应式对象 if (!ob) { target[key] = val return val } // 然后把新添加的属性变成响应式 defineReactive(ob.value, key, val) // 手动派发更新 ob.dep.notify() return val }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。