【面试题解析✨】Vue 的数据是如何渲染到页面的?

面试的时候,面试官经常会问 Vue 双向绑定的原理是什么我猜大部分人会跟我一样,不假思索的回答利用 Object.defineProperty 实现的。

其实这个回答很笼统,而且也没回答完整?Vue 中 Object.defineProperty 只是对数据做了劫持,具体的如何渲染到页面上,并没有考虑到。接下来从初始化开始,看看 Vue 都做了什么事情。

前提知识


在读源码前,需要了解 Object.defineProperty 的使用,以及 Vue Dep 的用法。这里就简单带过,各位大佬可以直接跳过,进行源码分析。

Object.defineProperty

当使用 Object.defineProperty 对对象的属性进行拦截时,调用该对象的属性,则会调用 get 函数,属性值则是 get 函数的返回值。当修改属性值时,则会调用 set 函数。

当然也可以通过 Object.defineProperty 给对象添加属性值,Vue 中就是通过这个方法将 datacomputed 等属性添加到 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 中的 getDep 的 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 函数进行页面的更新。

侵删,点击跳转原文

原文地址:https://www.cnblogs.com/Object-L/p/12574853.html