面试的时候,面试官经常会问 Vue 双向绑定的原理是什么?
我猜大部分人会跟我一样,不假思索的回答利用 Object.defineProperty
实现的。
其实这个回答很笼统,而且也没回答完整?Vue 中 Object.defineProperty
只是对数据做了劫持,具体的如何渲染到页面上,并没有考虑到。接下来从初始化开始,看看 Vue
都做了什么事情。
前提知识
在读源码前,需要了解 Object.defineProperty
的使用,以及 Vue Dep
的用法。这里就简单带过,各位大佬可以直接跳过,进行源码分析。
Object.defineProperty
当使用 Object.defineProperty
对对象的属性进行拦截时,调用该对象的属性,则会调用 get
函数,属性值则是 get
函数的返回值。当修改属性值时,则会调用 set
函数。
当然也可以通过 Object.defineProperty
给对象添加属性值,Vue 中就是通过这个方法将 data
、computed
等属性添加到 vm 上。
1 Object.defineProperty(obj, key, { 2 enumerable: true, 3 configurable: true, 4 get: function reactiveGetter () { 5 const value = getter ? getter.call(obj) : val 6 // 用于依赖收集,Dep 中讲到 7 if (Dep.target) { 8 dep.depend() 9 if (childOb) { 10 childOb.dep.depend() 11 if (Array.isArray(value)) { 12 dependArray(value) 13 } 14 } 15 } 16 return value 17 }, 18 set: function reactiveSetter (newVal) { 19 val = newVal 20 // val 发生变化时,发出通知,Dep 中讲到 21 dep.notify() 22 } 23 })
Dep
这里不讲什么设计模式了,直接看代码。
1 let uid = 0 2 3 export default class Dep { 4 static target: ?Watcher; 5 id: number; 6 subs: Array<Watcher>; 7 8 constructor () { 9 this.id = uid++ 10 this.subs = [] 11 } 12 13 addSub (sub: Watcher) { 14 // 添加 Watcher 15 this.subs.push(sub) 16 } 17 18 removeSub (sub: Watcher) { 19 // 从列表中移除某个 Watcher 20 remove(this.subs, sub) 21 } 22 23 depend () { 24 // 当 target 存在时,也就是目标 Watcher 存在的时候, 25 // 就可以为这个目标 Watcher 收集依赖 26 // Watcher 的 addDep 方法在下文中 27 if (Dep.target) { 28 Dep.target.addDep(this) 29 } 30 } 31 32 notify () { 33 // 对 Watcher 进行排序 34 const subs = this.subs.slice() 35 if (process.env.NODE_ENV !== 'production' && !config.async) { 36 subs.sort((a, b) => a.id - b.id) 37 } 38 // 当该依赖发生变化时, 调用添加到列表中的 Watcher 的 update 方法进行更新 39 for (let i = 0, l = subs.length; i < l; i++) { 40 subs[i].update() 41 } 42 } 43 } 44 45 // target 为某个 Watcher 实例,一次只能为一个 Watcher 收集依赖 46 Dep.target = null 47 // 通过堆栈存放 Watcher 实例, 48 // 当某个 Watcher 的实例未收集完,又有新的 Watcher 实例需要收集依赖, 49 // 那么旧的 Watcher 就先存放到 targetStack, 50 // 等待新的 Watcher 收集完后再为旧的 Watcher 收集 51 // 配合下面的 pushTarget 和 popTarget 实现 52 const targetStack = [] 53 54 export function pushTarget (target: ?Watcher) { 55 targetStack.push(target) 56 Dep.target = target 57 } 58 59 export function popTarget () { 60 targetStack.pop() 61 Dep.target = targetStack[targetStack.length - 1] 62 }
当某个 Watcher 需要依赖某个 dep 时,那么调用 dep.addSub(Watcher)
即可,当 dep 发生变化时,调用 dep.notify()
就可以触发 Watcher 的 update 方法。接下来看看 Vue 中 Watcher 的实现。
1 class Watcher { 2 // 很多属性,这里省略 3 ... 4 // 构造函数 5 constructor ( 6 vm: Component, 7 expOrFn: string | Function, 8 cb: Function, 9 options?: ?Object, 10 isRenderWatcher?: boolean 11 ) { ... } 12 13 get () { 14 // 当执行 Watcher 的 get 函数时,会将当前的 Watcher 作为 Dep 的 target 15 pushTarget(this) 16 let value 17 const vm = this.vm 18 try { 19 // 在执行 getter 时,当遇到响应式数据,会触发上面讲到的 Object.defineProperty 中的 get 函数 20 // Vue 就是在 Object.defineProperty 的 get 中调用 dep.depend() 进行依赖收集。 21 value = this.getter.call(vm, vm) 22 } catch (e) { 23 ... 24 } finally { 25 ... 26 // 当前 Watcher 的依赖收集完后,调用 popTarget 更换 Watcher 27 popTarget() 28 this.cleanupDeps() 29 } 30 return value 31 } 32 33 // dep.depend() 收集依赖时,会经过 Watcher 的 addDep 方法 34 // addDep 做了判断,避免重复收集,然后调用 dep.addSub 将该 Watcher 添加到 dep 的 subs 中 35 addDep (dep: Dep) { 36 const id = dep.id 37 if (!this.newDepIds.has(id)) { 38 this.newDepIds.add(id) 39 this.newDeps.push(dep) 40 if (!this.depIds.has(id)) { 41 dep.addSub(this) 42 } 43 } 44 } 45 }
通过 Object.defineProperty
中的 get
,Dep
的 depend
以及 Watcher
的 addDep
这三个函数的配合,完成了依赖的收集,就是将 Watcher
添加到 dep
的 subs
列表中。
当依赖发生变化时,就会调用 Object.defineProperty
中的 set
,在 set
中调用 dep
的 notify
,使得 subs
中的每个 Watcher
都执行 update
函数。
Watcher
中的 update
最终会重新调用 get
函数,重新求值并重新收集依赖。
源码分析
先看看 new Vue
都做了什么?
1 // vue/src/core/instance/index.js 2 function Vue (options) { 3 if (process.env.NODE_ENV !== 'production' && 4 !(this instanceof Vue) 5 ) { 6 // 只能使用 new Vue 调用该方法,否则输入警告 7 warn('Vue is a constructor and should be called with the `new` keyword') 8 } 9 // 开始初始化 10 this._init(options) 11 }
_init
方法通过原型挂载在 Vue 上
1 // vue/src/core/instance/init.js 2 export function initMixin (Vue: Class<Component>) { 3 Vue.prototype._init = function (options?: Object) { 4 const vm: Component = this 5 // a uid 6 vm._uid = uid++ 7 8 let startTag, endTag 9 // 初始化前打点,用于记录 Vue 实例初始化所消耗的时间 10 if (process.env.NODE_ENV !== 'production' && config.performance && mark) { 11 startTag = `vue-perf-start:${vm._uid}` 12 endTag = `vue-perf-end:${vm._uid}` 13 mark(startTag) 14 } 15 16 // a flag to avoid this being observed 17 vm._isVue = true 18 // 合并参数到 $options 19 if (options && options._isComponent) { 20 initInternalComponent(vm, options) 21 } else { 22 vm.$options = mergeOptions( 23 resolveConstructorOptions(vm.constructor), 24 options || {}, 25 vm 26 ) 27 } 28 29 if (process.env.NODE_ENV !== 'production') { 30 // 非生产环境以及支持 Proxy 的浏览器中,对 vm 的属性进行劫持,并将代理后的 vm 赋值给 _renderProxy 31 // 当调用 vm 不存在的属性时,进行错误提示。 32 // 在不支持 Proxy 的浏览器中,_renderProxy = vm; 为了简单理解,就看成等同于 vm 33 34 // 代码在 src/core/instance/proxy.js 35 initProxy(vm) 36 } else { 37 vm._renderProxy = vm 38 } 39 // expose real self 40 vm._self = vm 41 // 初始化声明周期函数 42 initLifecycle(vm) 43 // 初始化事件 44 initEvents(vm) 45 // 初始化 render 函数 46 initRender(vm) 47 // 触发 beforeCreate 钩子 48 callHook(vm, 'beforeCreate') 49 // 初始化 inject 50 initInjections(vm) // resolve injections before data/props 51 // 初始化 data/props 等 52 // 通过 Object.defineProperty 对数据进行劫持 53 initState(vm) 54 // 初始化 provide 55 initProvide(vm) // resolve provide after data/props 56 // 数据处理完后,触发 created 钩子 57 callHook(vm, 'created') 58 59 // 从 new Vue 到 created 所消耗的时间 60 if (process.env.NODE_ENV !== 'production' && config.performance && mark) { 61 vm._name = formatComponentName(vm, false) 62 mark(endTag) 63 measure(`vue ${vm._name} init`, startTag, endTag) 64 } 65 66 // 如果 options 有 el 参数则进行 mount 67 if (vm.$options.el) { 68 vm.$mount(vm.$options.el) 69 } 70 } 71 }
接下来进入 $mount
,因为用的是完整版的 Vue,直接看 vue/src/platforms/web/entry-runtime-with-compiler.js
这个文件。
1 // vue/src/platforms/web/entry-runtime-with-compiler.js 2 // 首先将 runtime 中的 $mount 方法赋值给 mount 进行保存 3 const mount = Vue.prototype.$mount 4 // 重写 $mount,对 template 编译为 render 函数后再调用 runtime 的 $mount 5 Vue.prototype.$mount = function ( 6 el?: string | Element, 7 hydrating?: boolean 8 ): Component { 9 el = el && query(el) 10 11 // 挂载元素不允许为 body 或 html 12 if (el === document.body || el === document.documentElement) { 13 process.env.NODE_ENV !== 'production' && warn( 14 `Do not mount Vue to <html> or <body> - mount to normal elements instead.` 15 ) 16 return this 17 } 18 19 const options = this.$options 20 if (!options.render) { 21 let template = options.template 22 // render 函数不存在时,将 template 转化为 render 函数 23 // 具体就不展开了 24 ... 25 if (template) { 26 ... 27 } else if (el) { 28 // template 不存在,则将 el 转成 template 29 // 从这里可以看出 Vue 支持 render、template、el 进行渲染 30 template = getOuterHTML(el) 31 } 32 if (template) { 33 const { render, staticRenderFns } = compileToFunctions(template, { 34 outputSourceRange: process.env.NODE_ENV !== 'production', 35 shouldDecodeNewlines, 36 shouldDecodeNewlinesForHref, 37 delimiters: options.delimiters, 38 comments: options.comments 39 }, this) 40 options.render = render 41 options.staticRenderFns = staticRenderFns 42 } 43 } 44 // 调用 runtime 中 $mount 45 return mount.call(this, el, hydrating) 46 }
查看 runtime 中的 $mount
1 // vue/src/platforms/web/runtime/index.js 2 Vue.prototype.$mount = function ( 3 el?: string | Element, 4 hydrating?: boolean 5 ): Component { 6 el = el && inBrowser ? query(el) : undefined 7 return mountComponent(this, el, hydrating) 8 }
mountComponent
定义在 vue/src/core/instance/lifecycle.js
中
1 // vue/src/core/instance/lifecycle.js 2 export function mountComponent ( 3 vm: Component, 4 el: ?Element, 5 hydrating?: boolean 6 ): Component { 7 vm.$el = el 8 if (!vm.$options.render) { 9 // 未定义 render 函数时,将 render 赋值为 createEmptyVNode 函数 10 vm.$options.render = createEmptyVNode 11 if (process.env.NODE_ENV !== 'production') { 12 /* istanbul ignore if */ 13 if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') || 14 vm.$options.el || el) { 15 // 用了 Vue 的 runtime 版本,而没有 render 函数时,报错处理 16 warn( 17 'You are using the runtime-only build of Vue where the template ' + 18 'compiler is not available. Either pre-compile the templates into ' + 19 'render functions, or use the compiler-included build.', 20 vm 21 ) 22 } else { 23 // template 和 render 都未定义时,报错处理 24 warn( 25 'Failed to mount component: template or render function not defined.', 26 vm 27 ) 28 } 29 } 30 } 31 // 调用 beforeMount 钩子 32 callHook(vm, 'beforeMount') 33 // 定义 updateComponent 函数 34 let updateComponent 35 /* istanbul ignore if */ 36 if (process.env.NODE_ENV !== 'production' && config.performance && mark) { 37 // 需要做监控性能时,在 updateComponent 内加入打点的操作 38 updateComponent = () => { 39 const name = vm._name 40 const id = vm._uid 41 const startTag = `vue-perf-start:${id}` 42 const endTag = `vue-perf-end:${id}` 43 44 mark(startTag) 45 const vnode = vm._render() 46 mark(endTag) 47 measure(`vue ${name} render`, startTag, endTag) 48 49 mark(startTag) 50 vm._update(vnode, hydrating) 51 mark(endTag) 52 measure(`vue ${name} patch`, startTag, endTag) 53 } 54 } else { 55 updateComponent = () => { 56 // updateComponent 主要调用 _update 进行浏览器渲染 57 // _render 返回 VNode 58 // 先继续往下看,等会再回来看这两个函数 59 vm._update(vm._render(), hydrating) 60 } 61 } 62 63 // new 一个渲染 Watcher 64 new Watcher(vm, updateComponent, noop, { 65 before () { 66 if (vm._isMounted && !vm._isDestroyed) { 67 callHook(vm, 'beforeUpdate') 68 } 69 } 70 }, true /* isRenderWatcher */) 71 hydrating = false 72 73 // 挂载完成,触发 mounted 74 if (vm.$vnode == null) { 75 vm._isMounted = true 76 callHook(vm, 'mounted') 77 } 78 return vm 79 }
先继续往下看,看看 new Watcher
做了什么,再回过头看 updateComponent
中的 _update
和 _render
。
Watcher
的构造函数如下
1 // vue/src/core/observer/watcher.js 2 constructor ( 3 vm: Component, 4 expOrFn: string | Function, 5 cb: Function, 6 options?: ?Object, 7 isRenderWatcher?: boolean 8 ) { 9 this.vm = vm 10 if (isRenderWatcher) { 11 vm._watcher = this 12 } 13 vm._watchers.push(this) 14 // options 15 if (options) { 16 this.deep = !!options.deep 17 this.user = !!options.user 18 this.lazy = !!options.lazy 19 this.sync = !!options.sync 20 this.before = options.before 21 } else { 22 this.deep = this.user = this.lazy = this.sync = false 23 } 24 this.cb = cb 25 this.id = ++uid // uid for batching 26 this.active = true 27 this.dirty = this.lazy // for lazy watchers 28 ... 29 // expOrFn 为上文的 updateComponent 函数,赋值给 getter 30 if (typeof expOrFn === 'function') { 31 this.getter = expOrFn 32 } else { 33 this.getter = parsePath(expOrFn) 34 if (!this.getter) { 35 this.getter = noop 36 ... 37 } 38 } 39 // lazy 为 false,调用 get 方法 40 this.value = this.lazy 41 ? undefined 42 : this.get() 43 } 44 45 // 执行 getter 函数,getter 函数为 updateComponent,并收集依赖 46 get () { 47 pushTarget(this) 48 let value 49 const vm = this.vm 50 try { 51 value = this.getter.call(vm, vm) 52 } catch (e) { 53 ... 54 } finally { 55 if (this.deep) { 56 traverse(value) 57 } 58 popTarget() 59 this.cleanupDeps() 60 } 61 return value 62 }
在 new Watcher
后会调用 updateComponent
函数,上文中 updateComponent
内执行了 vm._update
,_update
执行前会通过 _render
获得 vnode,接下里看看 _update
做了什么。_update
定义在 vue/src/core/instance/lifecycle.js
中
1 // vue/src/core/instance/lifecycle.js 2 Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { 3 const vm: Component = this 4 const prevVnode = vm._vnode 5 vm._vnode = vnode 6 ... 7 8 if (!prevVnode) { 9 // 初始渲染 10 vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) 11 } else { 12 // 更新 vnode 13 vm.$el = vm.__patch__(prevVnode, vnode) 14 } 15 ... 16 }
接下来到了 __patch__
函数进行页面渲染。
1 // vue/src/platforms/web/runtime/index.js 2 import { patch } from './patch' 3 Vue.prototype.__patch__ = inBrowser ? patch : noop
1 // vue/src/platforms/web/runtime/patch.js 2 import { createPatchFunction } from 'core/vdom/patch' 3 export const patch: Function = createPatchFunction({ nodeOps, modules })
createPatchFunction
提供了很多操作 virtual dom 的方法,最终会返回一个 path
函数。
1 export function createPatchFunction (backend) { 2 ... 3 // oldVnode 代表旧的节点,vnode 代表新的节点 4 return function patch (oldVnode, vnode, hydrating, removeOnly) { 5 // vnode 为 undefined, oldVnode 不为 undefined 则需要执行 destroy 6 if (isUndef(vnode)) { 7 if (isDef(oldVnode)) invokeDestroyHook(oldVnode) 8 return 9 } 10 11 let isInitialPatch = false 12 const insertedVnodeQueue = [] 13 14 if (isUndef(oldVnode)) { 15 // oldVnode 不存在,表示初始渲染,则根据 vnode 创建元素 16 isInitialPatch = true 17 createElm(vnode, insertedVnodeQueue) 18 } else { 19 20 const isRealElement = isDef(oldVnode.nodeType) 21 if (!isRealElement && sameVnode(oldVnode, vnode)) { 22 // oldVnode 与 vnode 为相同节点,调用 patchVnode 更新子节点 23 patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly) 24 } else { 25 if (isRealElement) { 26 // 服务端渲染的处理 27 ... 28 } 29 // 其他操作 30 ... 31 } 32 } 33 34 invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) 35 // 最终渲染到页面上 36 return vnode.elm 37 } 38 }
当渲染 Watcher 的依赖的数据发生变化时,会触发 Object.defineProperty
中的 set
函数。
从而调用 dep.notify()
通知该 Watcher 进行 update
操作。最终达到数据改变时,自动更新页面。 Watcher
的 update
函数就不再展开了,有兴趣的小伙伴可以自行查看。
最后再回过头看看前面遗留的 _render
函数。
1 updateComponent = () => { 2 vm._update(vm._render(), hydrating) 3 }
之前说了 _render
函数会返回 vnode
,看看具体做了什么吧。
1 // vue/src/core/instance/render.js 2 Vue.prototype._render = function (): VNode { 3 const vm: Component = this 4 // 从 $options 取出 render 函数以及 _parentVnode 5 // 这里的 render 函数可以是 template 或者 el 编译的 6 const { render, _parentVnode } = vm.$options 7 8 if (_parentVnode) { 9 vm.$scopedSlots = normalizeScopedSlots( 10 _parentVnode.data.scopedSlots, 11 vm.$slots, 12 vm.$scopedSlots 13 ) 14 } 15 16 vm.$vnode = _parentVnode 17 let vnode 18 try { 19 currentRenderingInstance = vm 20 // 最终会执行 $options 中的 render 函数 21 // _renderProxy 可以看做 vm 22 // 将 vm.$createElement 函数传递给 render,也就是经常看到的 h 函数 23 // 最终生成 vnode 24 vnode = render.call(vm._renderProxy, vm.$createElement) 25 } catch (e) { 26 // 异常处理 27 ... 28 } finally { 29 currentRenderingInstance = null 30 } 31 32 // 如果返回的数组只包含一个节点,则取第一个值 33 if (Array.isArray(vnode) && vnode.length === 1) { 34 vnode = vnode[0] 35 } 36 37 // vnode 如果不是 VNode 实例,报错并返回空的 vnode 38 if (!(vnode instanceof VNode)) { 39 if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) { 40 warn( 41 'Multiple root nodes returned from render function. Render function ' + 42 'should return a single root node.', 43 vm 44 ) 45 } 46 vnode = createEmptyVNode() 47 } 48 // 设置父节点 49 vnode.parent = _parentVnode 50 // 最终返回 vnode 51 return vnode 52 }
接下来就是看 vm.$createElement
也就是 render
函数中的 h
1 // vue/src/core/instance/render.js 2 import { createElement } from '../vdom/create-element' 3 export function initRender (vm: Component) { 4 ... 5 vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) 6 vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true) 7 ... 8 }
1 // vue/src/core/vdom/create-element.js 2 export function createElement ( 3 context: Component, 4 tag: any, 5 data: any, 6 children: any, 7 normalizationType: any, 8 alwaysNormalize: boolean 9 ): VNode | Array<VNode> { 10 // data 是数组或简单数据类型,代表 data 没传,将参数值赋值给正确的变量 11 if (Array.isArray(data) || isPrimitive(data)) { 12 normalizationType = children 13 children = data 14 data = undefined 15 } 16 if (isTrue(alwaysNormalize)) { 17 normalizationType = ALWAYS_NORMALIZE 18 } 19 // 将正确的参数传递给 _createElement 20 return _createElement(context, tag, data, children, normalizationType) 21 } 22 23 export function _createElement ( 24 context: Component, 25 tag?: string | Class<Component> | Function | Object, 26 data?: VNodeData, 27 children?: any, 28 normalizationType?: number 29 ): VNode | Array<VNode> { 30 if (isDef(data) && isDef((data: any).__ob__)) { 31 // render 函数中的 data 不能为响应式数据 32 process.env.NODE_ENV !== 'production' && warn( 33 `Avoid using observed data object as vnode data: ${JSON.stringify(data)} ` + 34 'Always create fresh vnode data objects in each render!', 35 context 36 ) 37 // 返回空的 vnode 节点 38 return createEmptyVNode() 39 } 40 // 用 is 指定标签 41 if (isDef(data) && isDef(data.is)) { 42 tag = data.is 43 } 44 if (!tag) { 45 // in case of component :is set to falsy value 46 return createEmptyVNode() 47 } 48 // key 值不是简单数据类型时,警告提示 49 if (process.env.NODE_ENV !== 'production' && 50 isDef(data) && isDef(data.key) && !isPrimitive(data.key) 51 ) { ... } 52 53 if (Array.isArray(children) && 54 typeof children[0] === 'function' 55 ) { 56 data = data || {} 57 data.scopedSlots = { default: children[0] } 58 children.length = 0 59 } 60 // 处理子节点 61 if (normalizationType === ALWAYS_NORMALIZE) { 62 // VNode 数组 63 children = normalizeChildren(children) 64 } else if (normalizationType === SIMPLE_NORMALIZE) { 65 children = simpleNormalizeChildren(children) 66 } 67 68 // 生成 vnode 69 let vnode, ns 70 if (typeof tag === 'string') { 71 let Ctor 72 ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag) 73 if (config.isReservedTag(tag)) { 74 ... 75 vnode = new VNode( 76 config.parsePlatformTagName(tag), data, children, 77 undefined, undefined, context 78 ) 79 } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { 80 vnode = createComponent(Ctor, data, context, children, tag) 81 } else { 82 vnode = new VNode( 83 tag, data, children, 84 undefined, undefined, context 85 ) 86 } 87 } else { 88 vnode = createComponent(tag, data, context, children) 89 } 90 91 // 返回 vnode 92 if (Array.isArray(vnode)) { 93 return vnode 94 } else if (isDef(vnode)) { 95 if (isDef(ns)) applyNS(vnode, ns) 96 if (isDef(data)) registerDeepBindings(data) 97 return vnode 98 } else { 99 return createEmptyVNode() 100 } 101 }
总结
代码看起来很多,其实主要流程可以分为以下 4 点:
1、 new Vue
初始化数据等
2、$mount
将 render、template 或 el 转为 render 函数
3、生成一个渲染 Watcher 收集依赖,并将执行 render 函数生成 vnode 传递给 patch 函数执行,渲染页面。
4、当渲染 Watcher 依赖发生变化时,执行 Watcher 的 getter 函数,重新依赖收集。并且重新执行 render 函数生成 vnode 传递给 patch 函数进行页面的更新。
侵删,点击跳转原文