前言

Vnode也称虚拟node节点,是对真实元素的抽象。诞生的背景是因为前端在很长一段时间通过直接操作Dom来达到修改视图,随着项目庞大,维护就变成一个问题。那换个角度想如果把真实Dom树抽象成为一棵以JS语法构建的抽象,然后通过修改抽象树的结构来转换成真实的Dom来重新渲染到视图。
所谓虚拟DOM,是一个用于表示真实 DOM 结构和属性的 JavaScript 对象,这个对象用于对比虚拟 DOM 和当前真实 DOM 的差异化,然后进行局部渲染从而实现性能上的优化。在Vue.js 中虚拟 DOM 的 JavaScript 对象就是 VNode。

如何生成虚拟节点

定义

既然是虚拟 DOM 的作用是转为真实的 DOM,那这就是一个渲染的过程。所以我们看看 render 方法。在之前的学习中我们知道了,vue 的渲染函数 _render 方法返回的就是一个 VNode 对象。而在 initRender 初始化渲染的方法中定义的 vm._cvm.$createElement 方法中,createElement 最终也是返回 VNode 对象。所以 VNode 是渲染的关键所在。

  1. var VNode = function VNode (
  2. tag,
  3. data,
  4. children,
  5. text,
  6. elm,
  7. context,
  8. componentOptions,
  9. asyncFactory
  10. ) {
  11. this.tag = tag; //当前节点的标签名
  12. this.data = data; //当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息
  13. this.children = children; //当前节点的子节点,是一个数组
  14. this.text = text; //当前节点的文本
  15. this.elm = elm; //当前虚拟节点对应的真实dom节点
  16. this.ns = undefined; //当前节点的名字空间
  17. this.context = context; //当前节点的编译作用域
  18. this.fnContext = undefined; //函数化组件作用域
  19. this.fnOptions = undefined;
  20. this.fnScopeId = undefined;
  21. this.key = data && data.key; //节点的key属性,被当作节点的标志,用以优化
  22. this.componentOptions = componentOptions;
  23. this.componentInstance = undefined;
  24. this.parent = undefined; //当前节点的父节点
  25. this.raw = false; //简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false
  26. this.isStatic = false;
  27. this.isRootInsert = true;
  28. this.isComment = false; //是否为注释节点
  29. this.isCloned = false;
  30. this.isOnce = false;
  31. this.asyncFactory = asyncFactory; //异步工厂方法
  32. this.asyncMeta = undefined; //异步Meta
  33. this.isAsyncPlaceholder = false; //是否为异步占位
  34. };

实例

  1. <div class="test">
  2. <span class="demo">hello,VNode</span>
  3. </div>

VNode树

  1. {
  2. tag: 'div'
  3. data: {
  4. class: 'test'
  5. },
  6. children: [
  7. {
  8. tag: 'span',
  9. data: {
  10. class: 'demo'
  11. }
  12. text: 'hello,VNode'
  13. }
  14. ]
  15. }
  1. <div id="app">
  2. <span>{{ message }}</span>
  3. <ul>
  4. <li v-for="item of list" class="item-cls">{{ item }}</li>
  5. </ul>
  6. </div>
  7. <script>
  8. new Vue({
  9. el: '#app',
  10. data: {
  11. message: 'hello Vue.js',
  12. list: ['jack', 'rose', 'james']
  13. }
  14. })
  15. </script>

VNode树

  1. {
  2. "tag": "div",
  3. "data": {
  4. "attr": { "id": "app" }
  5. },
  6. "children": [
  7. {
  8. "tag": "span",
  9. "children": [
  10. { "text": "hello Vue.js" }
  11. ]
  12. },
  13. {
  14. "tag": "ul",
  15. "children": [
  16. {
  17. "tag": "li",
  18. "data": { "staticClass": "item-cls" },
  19. "children": [
  20. { "text": "jack" }
  21. ]
  22. },
  23. {
  24. "tag": "li",
  25. "data": { "staticClass": "item-cls" },
  26. "children": [
  27. { "text": "rose" }
  28. ]
  29. },
  30. {
  31. "tag": "li",
  32. "data": { "staticClass": "item-cls" },
  33. "children": [
  34. { "text": "james" }
  35. ]
  36. }
  37. ]
  38. }
  39. ],
  40. "context": "$Vue$3",
  41. "elm": "div#app"
  42. }

在看VNode的时候小结以下几点:

  • 所有对象的 context 选项都指向了 Vue 实例。
  • elm 属性则指向了其相对应的真实 DOM 节点。
  • DOM 中的文本内容被当做了一个只有 text 没有 tag 的节点。
  • 像class、id等HTML属性都放在了 data

createEmptyVNode

创建一个空VNode节点

  1. var createEmptyVNode = function (text) {
  2. if ( text === void 0 ) text = '';
  3. var node = new VNode();
  4. node.text = text;
  5. node.isComment = true;
  6. return node
  7. };

void的运用 void 运算符 对给定的表达式进行求值,然后返回undefined。void 运算符通常只用于获取 undefined的原始值,一般使用void(0)(等同于void 0)。在上述情况中,也可以使用全局变量undefined来代替(假定其仍是默认值)。 undefined的定义

  • 未初始化的变量
  • 访问对象不存在的属性
  • 数组越界访问
  • 缺省的函数参数
  • 调用没有任何返回值的函数
  • void 操作符

createTextVNode

创建一个文本节点

  1. function createTextVNode (val) {
  2. return new VNode(undefined, undefined, undefined, String(val))
  3. }

cloneVNode

克隆一个VNode节点

  1. function cloneVNode (vnode) {
  2. var cloned = new VNode(
  3. vnode.tag,
  4. vnode.data,
  5. // #7975
  6. // clone children array to avoid mutating original in case of cloning
  7. // a child.
  8. vnode.children && vnode.children.slice(),
  9. vnode.text,
  10. vnode.elm,
  11. vnode.context,
  12. vnode.componentOptions,
  13. vnode.asyncFactory
  14. );
  15. cloned.ns = vnode.ns;
  16. cloned.isStatic = vnode.isStatic;
  17. cloned.key = vnode.key;
  18. cloned.isComment = vnode.isComment;
  19. cloned.fnContext = vnode.fnContext;
  20. cloned.fnOptions = vnode.fnOptions;
  21. cloned.fnScopeId = vnode.fnScopeId;
  22. cloned.asyncMeta = vnode.asyncMeta;
  23. cloned.isCloned = true;
  24. return cloned
  25. }

createElement

创建元素。
createElement用来创建一个虚拟节点。当data上已经绑定ob的时候,代表该对象已经被Oberver过了,所以创建一个空节点。tag不存在的时候同样创建一个空节点。当tag不是一个String类型的时候代表tag是一个组件的构造类,直接用new VNode创建。当tag是String类型的时候,如果是保留标签,则用new VNode创建一个VNode实例,如果在vm的option的components找得到该tag,代表这是一个组件,否则统一用new VNode创建。

  1. function createElement (
  2. context,
  3. tag,
  4. data,
  5. children,
  6. normalizationType,
  7. alwaysNormalize
  8. ) {
  9. if (Array.isArray(data) || isPrimitive(data)) {
  10. normalizationType = children;
  11. children = data;
  12. data = undefined;
  13. }
  14. if (isTrue(alwaysNormalize)) {
  15. normalizationType = ALWAYS_NORMALIZE;
  16. }
  17. return _createElement(context, tag, data, children, normalizationType)
  18. }
  19. function _createElement (
  20. context,
  21. tag,
  22. data,
  23. children,
  24. normalizationType
  25. ) {
  26. if (isDef(data) && isDef((data).__ob__)) {
  27. warn(
  28. "Avoid using observed data object as vnode data: " + (JSON.stringify(data)) + "\n" +
  29. 'Always create fresh vnode data objects in each render!',
  30. context
  31. );
  32. return createEmptyVNode()
  33. }
  34. // object syntax in v-bind
  35. if (isDef(data) && isDef(data.is)) {
  36. tag = data.is;
  37. }
  38. if (!tag) {
  39. // in case of component :is set to falsy value
  40. return createEmptyVNode()
  41. }
  42. // warn against non-primitive key
  43. if (isDef(data) && isDef(data.key) && !isPrimitive(data.key)
  44. ) {
  45. {
  46. warn(
  47. 'Avoid using non-primitive value as key, ' +
  48. 'use string/number value instead.',
  49. context
  50. );
  51. }
  52. }
  53. // support single function children as default scoped slot
  54. if (Array.isArray(children) &&
  55. typeof children[0] === 'function'
  56. ) {
  57. data = data || {};
  58. data.scopedSlots = { default: children[0] };
  59. children.length = 0;
  60. }
  61. if (normalizationType === ALWAYS_NORMALIZE) {
  62. children = normalizeChildren(children);
  63. } else if (normalizationType === SIMPLE_NORMALIZE) {
  64. children = simpleNormalizeChildren(children);
  65. }
  66. var vnode, ns;
  67. if (typeof tag === 'string') {
  68. var Ctor;
  69. ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
  70. if (config.isReservedTag(tag)) {
  71. // platform built-in elements
  72. if (isDef(data) && isDef(data.nativeOn)) {
  73. warn(
  74. ("The .native modifier for v-on is only valid on components but it was used on <" + tag + ">."),
  75. context
  76. );
  77. }
  78. vnode = new VNode(
  79. config.parsePlatformTagName(tag), data, children,
  80. undefined, undefined, context
  81. );
  82. } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
  83. // component
  84. vnode = createComponent(Ctor, data, context, children, tag);
  85. } else {
  86. // unknown or unlisted namespaced elements
  87. // check at runtime because it may get assigned a namespace when its
  88. // parent normalizes children
  89. vnode = new VNode(
  90. tag, data, children,
  91. undefined, undefined, context
  92. );
  93. }
  94. } else {
  95. // direct component options / constructor
  96. vnode = createComponent(tag, data, context, children);
  97. }
  98. if (Array.isArray(vnode)) {
  99. return vnode
  100. } else if (isDef(vnode)) {
  101. if (isDef(ns)) { applyNS(vnode, ns); }
  102. if (isDef(data)) { registerDeepBindings(data); }
  103. return vnode
  104. } else {
  105. return createEmptyVNode()
  106. }
  107. }
  1. function isDef (v) {
  2. return v !== undefined && v !== null
  3. }
  4. /**
  5. * Observer class that is attached to each observed
  6. * object. Once attached, the observer converts the target
  7. * object's property keys into getter/setters that
  8. * collect dependencies and dispatch updates.
  9. */
  10. var Observer = function Observer (value) {
  11. this.value = value;
  12. this.dep = new Dep();
  13. this.vmCount = 0;
  14. def(value, '__ob__', this);
  15. if (Array.isArray(value)) {
  16. if (hasProto) {
  17. protoAugment(value, arrayMethods);
  18. } else {
  19. copyAugment(value, arrayMethods, arrayKeys);
  20. }
  21. this.observeArray(value);
  22. } else {
  23. this.walk(value);
  24. }
  25. };
  26. function def (obj, key, val, enumerable) {
  27. Object.defineProperty(obj, key, {
  28. value: val,
  29. enumerable: !!enumerable,
  30. writable: true,
  31. configurable: true
  32. });
  33. }

createComponent

创建组件节点

  1. function createComponent (
  2. Ctor,
  3. data,
  4. context,
  5. children,
  6. tag
  7. ) {
  8. if (isUndef(Ctor)) {
  9. return
  10. }
  11. var baseCtor = context.$options._base;
  12. // plain options object: turn it into a constructor
  13. if (isObject(Ctor)) {
  14. Ctor = baseCtor.extend(Ctor);
  15. }
  16. // if at this stage it's not a constructor or an async component factory,
  17. // reject.
  18. if (typeof Ctor !== 'function') {
  19. {
  20. warn(("Invalid Component definition: " + (String(Ctor))), context);
  21. }
  22. return
  23. }
  24. // async component
  25. var asyncFactory;
  26. if (isUndef(Ctor.cid)) {
  27. asyncFactory = Ctor;
  28. Ctor = resolveAsyncComponent(asyncFactory, baseCtor);
  29. if (Ctor === undefined) {
  30. // return a placeholder node for async component, which is rendered
  31. // as a comment node but preserves all the raw information for the node.
  32. // the information will be used for async server-rendering and hydration.
  33. return createAsyncPlaceholder(
  34. asyncFactory,
  35. data,
  36. context,
  37. children,
  38. tag
  39. )
  40. }
  41. }
  42. data = data || {};
  43. // resolve constructor options in case global mixins are applied after
  44. // component constructor creation
  45. resolveConstructorOptions(Ctor);
  46. // transform component v-model data into props & events
  47. if (isDef(data.model)) {
  48. transformModel(Ctor.options, data);
  49. }
  50. // extract props
  51. var propsData = extractPropsFromVNodeData(data, Ctor, tag);
  52. // functional component
  53. if (isTrue(Ctor.options.functional)) {
  54. return createFunctionalComponent(Ctor, propsData, data, context, children)
  55. }
  56. // extract listeners, since these needs to be treated as
  57. // child component listeners instead of DOM listeners
  58. var listeners = data.on;
  59. // replace with listeners with .native modifier
  60. // so it gets processed during parent component patch.
  61. data.on = data.nativeOn;
  62. if (isTrue(Ctor.options.abstract)) {
  63. // abstract components do not keep anything
  64. // other than props & listeners & slot
  65. // work around flow
  66. var slot = data.slot;
  67. data = {};
  68. if (slot) {
  69. data.slot = slot;
  70. }
  71. }
  72. // install component management hooks onto the placeholder node
  73. installComponentHooks(data);
  74. // return a placeholder vnode
  75. var name = Ctor.options.name || tag;
  76. var vnode = new VNode(
  77. ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
  78. data, undefined, undefined, undefined, context,
  79. { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
  80. asyncFactory
  81. );
  82. return vnode
  83. }

检测变化并更新视图patch

Vue的渲染过程(无论是初始化视图还是更新视图)最终都将走到 _update 方法中,再来看看这个 _update 方法。

  1. Vue.prototype._update = function (vnode, hydrating) {
  2. var vm = this;
  3. var prevEl = vm.$el;
  4. var prevVnode = vm._vnode;
  5. var restoreActiveInstance = setActiveInstance(vm);
  6. vm._vnode = vnode;
  7. // Vue.prototype.__patch__ is injected in entry points
  8. // based on the rendering backend used.
  9. if (!prevVnode) {
  10. // initial render 初始化渲染
  11. vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
  12. } else {
  13. // updates 更新渲染
  14. vm.$el = vm.__patch__(prevVnode, vnode);
  15. }
  16. restoreActiveInstance();
  17. // update __vue__ reference
  18. if (prevEl) {
  19. prevEl.__vue__ = null;
  20. }
  21. if (vm.$el) {
  22. vm.$el.__vue__ = vm;
  23. }
  24. // if parent is an HOC, update its $el as well
  25. if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
  26. vm.$parent.$el = vm.$el;
  27. }
  28. // updated hook is called by the scheduler to ensure that children are
  29. // updated in a parent's updated hook.
  30. };

不难发现更新试图都是使用了 vm.__patch__ 方法,我们继续往下跟。

  1. // install platform patch function
  2. Vue.prototype.__patch__ = inBrowser ? patch : noop;
  3. var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules });
  1. function createPatchFunction (backend) {
  2. ...
  3. function createElm (
  4. vnode,
  5. insertedVnodeQueue,
  6. parentElm,
  7. refElm,
  8. nested,
  9. ownerArray,
  10. index
  11. ) {
  12. if (isDef(vnode.elm) && isDef(ownerArray)) {
  13. // This vnode was used in a previous render!
  14. // now it's used as a new node, overwriting its elm would cause
  15. // potential patch errors down the road when it's used as an insertion
  16. // reference node. Instead, we clone the node on-demand before creating
  17. // associated DOM element for it.
  18. vnode = ownerArray[index] = cloneVNode(vnode);
  19. }
  20. vnode.isRootInsert = !nested; // for transition enter check
  21. if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
  22. return
  23. }
  24. var data = vnode.data;
  25. var children = vnode.children;
  26. var tag = vnode.tag;
  27. if (isDef(tag)) {
  28. {
  29. if (data && data.pre) {
  30. creatingElmInVPre++;
  31. }
  32. if (isUnknownElement$$1(vnode, creatingElmInVPre)) {
  33. warn(
  34. 'Unknown custom element: <' + tag + '> - did you ' +
  35. 'register the component correctly? For recursive components, ' +
  36. 'make sure to provide the "name" option.',
  37. vnode.context
  38. );
  39. }
  40. }
  41. vnode.elm = vnode.ns
  42. ? nodeOps.createElementNS(vnode.ns, tag)
  43. : nodeOps.createElement(tag, vnode);
  44. setScope(vnode);
  45. /* istanbul ignore if */
  46. {
  47. createChildren(vnode, children, insertedVnodeQueue);
  48. if (isDef(data)) {
  49. invokeCreateHooks(vnode, insertedVnodeQueue);
  50. }
  51. insert(parentElm, vnode.elm, refElm);
  52. }
  53. if (data && data.pre) {
  54. creatingElmInVPre--;
  55. }
  56. } else if (isTrue(vnode.isComment)) {
  57. vnode.elm = nodeOps.createComment(vnode.text);
  58. insert(parentElm, vnode.elm, refElm);
  59. } else {
  60. vnode.elm = nodeOps.createTextNode(vnode.text);
  61. insert(parentElm, vnode.elm, refElm);
  62. }
  63. }
  64. return function patch (oldVnode, vnode, hydrating, removeOnly) {
  65. // 当前 vnode 未定义、老的 VNode 定义了,调用销毁钩子
  66. if (isUndef(vnode)) {
  67. if (isDef(oldVnode)) { invokeDestroyHook(oldVnode); }
  68. return
  69. }
  70. var isInitialPatch = false;
  71. var insertedVnodeQueue = [];
  72. if (isUndef(oldVnode)) {
  73. // empty mount (likely as component), create new root element 老的 VNode 未定义,初始化
  74. isInitialPatch = true;
  75. createElm(vnode, insertedVnodeQueue);
  76. } else {
  77. //当前 VNode 和老 VNode 都定义了,执行更新操作
  78. var isRealElement = isDef(oldVnode.nodeType);
  79. if (!isRealElement && sameVnode(oldVnode, vnode)) {
  80. // patch existing root node 修改已有根节点
  81. patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
  82. } else {
  83. // 已有真实 DOM 元素,处理 oldVnode
  84. if (isRealElement) {
  85. // mounting to a real element
  86. // check if this is server-rendered content and if we can perform
  87. // a successful hydration.
  88. // 挂载一个真实元素,确认是否为服务器渲染环境或者是否可以执行成功的合并到真实 DOM 中
  89. if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
  90. oldVnode.removeAttribute(SSR_ATTR);
  91. hydrating = true;
  92. }
  93. if (isTrue(hydrating)) {
  94. if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
  95. // 调用 insert 钩子
  96. // inserted:被绑定元素插入父节点时调用
  97. invokeInsertHook(vnode, insertedVnodeQueue, true);
  98. return oldVnode
  99. } else {
  100. warn(
  101. 'The client-side rendered virtual DOM tree is not matching ' +
  102. 'server-rendered content. This is likely caused by incorrect ' +
  103. 'HTML markup, for example nesting block-level elements inside ' +
  104. '<p>, or missing <tbody>. Bailing hydration and performing ' +
  105. 'full client-side render.'
  106. );
  107. }
  108. }
  109. // either not server-rendered, or hydration failed.
  110. // create an empty node and replace it
  111. // 不是服务器渲染或者合并到真实 DOM 失败,创建一个空节点替换原有节点
  112. oldVnode = emptyNodeAt(oldVnode);
  113. }
  114. // replacing existing element
  115. // 替换已有元素
  116. var oldElm = oldVnode.elm;
  117. var parentElm = nodeOps.parentNode(oldElm);
  118. // create new node
  119. // 创建新节点
  120. createElm(
  121. vnode,
  122. insertedVnodeQueue,
  123. // extremely rare edge case: do not insert if old element is in a
  124. // leaving transition. Only happens when combining transition +
  125. // keep-alive + HOCs. (#4590)
  126. oldElm._leaveCb ? null : parentElm,
  127. nodeOps.nextSibling(oldElm)
  128. );
  129. // update parent placeholder node element, recursively
  130. // 递归更新父级占位节点元素
  131. if (isDef(vnode.parent)) {
  132. var ancestor = vnode.parent;
  133. var patchable = isPatchable(vnode);
  134. while (ancestor) {
  135. for (var i = 0; i < cbs.destroy.length; ++i) {
  136. cbs.destroy[i](ancestor);
  137. }
  138. ancestor.elm = vnode.elm;
  139. if (patchable) {
  140. for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
  141. cbs.create[i$1](emptyNode, ancestor);
  142. }
  143. // #6513
  144. // invoke insert hooks that may have been merged by create hooks.
  145. // e.g. for directives that uses the "inserted" hook.
  146. var insert = ancestor.data.hook.insert;
  147. if (insert.merged) {
  148. // start at index 1 to avoid re-invoking component mounted hook
  149. for (var i$2 = 1; i$2 < insert.fns.length; i$2++) {
  150. insert.fns[i$2]();
  151. }
  152. }
  153. } else {
  154. registerRef(ancestor);
  155. }
  156. ancestor = ancestor.parent;
  157. }
  158. }
  159. // destroy old node
  160. // 销毁旧节点
  161. if (isDef(parentElm)) {
  162. removeVnodes([oldVnode], 0, 0);
  163. } else if (isDef(oldVnode.tag)) {
  164. invokeDestroyHook(oldVnode);
  165. }
  166. }
  167. }
  168. // 调用 insert 钩子
  169. invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
  170. return vnode.elm
  171. }
  172. }

createElm

这个方法创建了真实 DOM 元素

  1. function createElm (
  2. vnode,
  3. insertedVnodeQueue,
  4. parentElm,
  5. refElm,
  6. nested,
  7. ownerArray,
  8. index
  9. ) {
  10. if (isDef(vnode.elm) && isDef(ownerArray)) {
  11. // This vnode was used in a previous render!
  12. // now it's used as a new node, overwriting its elm would cause
  13. // potential patch errors down the road when it's used as an insertion
  14. // reference node. Instead, we clone the node on-demand before creating
  15. // associated DOM element for it.
  16. vnode = ownerArray[index] = cloneVNode(vnode);
  17. }
  18. vnode.isRootInsert = !nested; // for transition enter check
  19. if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
  20. return
  21. }
  22. var data = vnode.data;
  23. var children = vnode.children;
  24. var tag = vnode.tag;
  25. if (isDef(tag)) {
  26. {
  27. if (data && data.pre) {
  28. creatingElmInVPre++;
  29. }
  30. if (isUnknownElement$$1(vnode, creatingElmInVPre)) {
  31. warn(
  32. 'Unknown custom element: <' + tag + '> - did you ' +
  33. 'register the component correctly? For recursive components, ' +
  34. 'make sure to provide the "name" option.',
  35. vnode.context
  36. );
  37. }
  38. }
  39. vnode.elm = vnode.ns
  40. ? nodeOps.createElementNS(vnode.ns, tag)
  41. : nodeOps.createElement(tag, vnode);
  42. setScope(vnode);
  43. /* istanbul ignore if */
  44. {
  45. createChildren(vnode, children, insertedVnodeQueue);
  46. if (isDef(data)) {
  47. invokeCreateHooks(vnode, insertedVnodeQueue);
  48. }
  49. insert(parentElm, vnode.elm, refElm);
  50. }
  51. if (data && data.pre) {
  52. creatingElmInVPre--;
  53. }
  54. } else if (isTrue(vnode.isComment)) {
  55. vnode.elm = nodeOps.createComment(vnode.text);
  56. insert(parentElm, vnode.elm, refElm);
  57. } else {
  58. vnode.elm = nodeOps.createTextNode(vnode.text);
  59. insert(parentElm, vnode.elm, refElm);
  60. }
  61. }

重点关注代码中的方法执行。代码太多,就不贴出来了,简单说说用途。

  • cloneVNode 用于克隆当前 vnode 对象。
  • createComponent 用于创建组件,在调用了组件初始化钩子之后,初始化组件,并且重新激活组件。在重新激活组件中使用 insert 方法操作 DOM。
  • nodeOps.createElementNSnodeOps.createElement 方法,其实是真实 DOM 的方法。
  • setScope 用于为 scoped CSS 设置作用域 ID 属性
  • createChildren 用于创建子节点,如果子节点是数组,则遍历执行 createElm 方法,如果子节点的 text 属性有数据,则使用 nodeOps.appendChild(…) 在真实 DOM 中插入文本内容。
  • insert 用于将元素插入真实 DOM 中。

所以,这里的 nodeOps 指的肯定就是真实的 DOM 节点了。最终,这些所有的方法都调用了 nodeOps 中的方法来操作 DOM 元素。

这里顺便科普下 DOM 的属性和方法。下面把源码中用到的几个方法列出来便于学习:

  • appendChild: 向元素添加新的子节点,作为最后一个子节点。
  • insertBefore: 在指定的已有的子节点之前插入新节点。
  • tagName: 返回元素的标签名。
  • removeChild: 从元素中移除子节点。
  • createElementNS: 创建带有指定命名空间的元素节点。
  • createElement: 创建元素节点。
  • createComment: 创建注释节点。
  • createTextNode: 创建文本节点。
  • setAttribute: 把指定属性设置或更改为指定值。
  • nextSibling: 返回位于相同节点树层级的下一个节点。
  • parentNode: 返回元素父节点。
  • setTextContent: 获取文本内容(这个未在w3school中找到,不过应该就是这个意思了)。

patchVnode

看过了创建真实 DOM 后,我们来学习虚拟 DOM 如何实现 DOM 的更新。这才是虚拟 DOM 的存在意义 —— 比对并局部更新 DOM 以达到性能优化的目的。 看代码~

  1. function patchVnode (
  2. oldVnode,
  3. vnode,
  4. insertedVnodeQueue,
  5. ownerArray,
  6. index,
  7. removeOnly
  8. ) {
  9. // 新旧 vnode 相等
  10. if (oldVnode === vnode) {
  11. return
  12. }
  13. if (isDef(vnode.elm) && isDef(ownerArray)) {
  14. // clone reused vnode
  15. vnode = ownerArray[index] = cloneVNode(vnode);
  16. }
  17. var elm = vnode.elm = oldVnode.elm;
  18. // 异步占位
  19. if (isTrue(oldVnode.isAsyncPlaceholder)) {
  20. if (isDef(vnode.asyncFactory.resolved)) {
  21. hydrate(oldVnode.elm, vnode, insertedVnodeQueue);
  22. } else {
  23. vnode.isAsyncPlaceholder = true;
  24. }
  25. return
  26. }
  27. // reuse element for static trees.
  28. // note we only do this if the vnode is cloned -
  29. // if the new node is not cloned it means the render functions have been
  30. // reset by the hot-reload-api and we need to do a proper re-render.
  31. // 如果新旧 vnode 为静态;新旧 vnode key相同;
  32. // 新 vnode 是克隆所得;新 vnode 有 v-once 的属性
  33. // 则新 vnode 的 componentInstance 用老的 vnode 的。
  34. // 即 vnode 的 componentInstance 保持不变。
  35. if (isTrue(vnode.isStatic) &&
  36. isTrue(oldVnode.isStatic) &&
  37. vnode.key === oldVnode.key &&
  38. (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
  39. ) {
  40. vnode.componentInstance = oldVnode.componentInstance;
  41. return
  42. }
  43. var i;
  44. var data = vnode.data;
  45. if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
  46. i(oldVnode, vnode);
  47. }
  48. var oldCh = oldVnode.children;
  49. var ch = vnode.children;
  50. if (isDef(data) && isPatchable(vnode)) {
  51. for (i = 0; i < cbs.update.length; ++i) { cbs.update[i](oldVnode, vnode); }
  52. if (isDef(i = data.hook) && isDef(i = i.update)) { i(oldVnode, vnode); }
  53. }
  54. if (isUndef(vnode.text)) {
  55. if (isDef(oldCh) && isDef(ch)) {
  56. // 新旧 vnode 都有 children,且不同,执行 updateChildren 方法。
  57. if (oldCh !== ch) { updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); }
  58. } else if (isDef(ch)) {
  59. {
  60. checkDuplicateKeys(ch);
  61. }
  62. if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, ''); }
  63. addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
  64. } else if (isDef(oldCh)) {
  65. removeVnodes(oldCh, 0, oldCh.length - 1);
  66. } else if (isDef(oldVnode.text)) {
  67. nodeOps.setTextContent(elm, '');
  68. }
  69. } else if (oldVnode.text !== vnode.text) {
  70. nodeOps.setTextContent(elm, vnode.text);
  71. }
  72. if (isDef(data)) {
  73. if (isDef(i = data.hook) && isDef(i = i.postpatch)) { i(oldVnode, vnode); }
  74. }
  75. }

updateChildren

  1. function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
  2. var oldStartIdx = 0;
  3. var newStartIdx = 0;
  4. var oldEndIdx = oldCh.length - 1;
  5. var oldStartVnode = oldCh[0];
  6. var oldEndVnode = oldCh[oldEndIdx];
  7. var newEndIdx = newCh.length - 1;
  8. var newStartVnode = newCh[0];
  9. var newEndVnode = newCh[newEndIdx];
  10. var oldKeyToIdx, idxInOld, vnodeToMove, refElm;
  11. // removeOnly is a special flag used only by <transition-group>
  12. // to ensure removed elements stay in correct relative positions
  13. // during leaving transitions
  14. var canMove = !removeOnly;
  15. {
  16. checkDuplicateKeys(newCh);
  17. }
  18. while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  19. if (isUndef(oldStartVnode)) {
  20. oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
  21. } else if (isUndef(oldEndVnode)) {
  22. oldEndVnode = oldCh[--oldEndIdx];
  23. } else if (sameVnode(oldStartVnode, newStartVnode)) {
  24. patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
  25. oldStartVnode = oldCh[++oldStartIdx];
  26. newStartVnode = newCh[++newStartIdx];
  27. } else if (sameVnode(oldEndVnode, newEndVnode)) {
  28. patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
  29. oldEndVnode = oldCh[--oldEndIdx];
  30. newEndVnode = newCh[--newEndIdx];
  31. } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
  32. patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
  33. canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));
  34. oldStartVnode = oldCh[++oldStartIdx];
  35. newEndVnode = newCh[--newEndIdx];
  36. } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
  37. patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
  38. canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
  39. oldEndVnode = oldCh[--oldEndIdx];
  40. newStartVnode = newCh[++newStartIdx];
  41. } else {
  42. if (isUndef(oldKeyToIdx)) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); }
  43. idxInOld = isDef(newStartVnode.key)
  44. ? oldKeyToIdx[newStartVnode.key]
  45. : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
  46. if (isUndef(idxInOld)) { // New element
  47. createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
  48. } else {
  49. vnodeToMove = oldCh[idxInOld];
  50. if (sameVnode(vnodeToMove, newStartVnode)) {
  51. patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
  52. oldCh[idxInOld] = undefined;
  53. canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
  54. } else {
  55. // same key but different element. treat as new element
  56. createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
  57. }
  58. }
  59. newStartVnode = newCh[++newStartIdx];
  60. }
  61. }
  62. if (oldStartIdx > oldEndIdx) {
  63. refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
  64. addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
  65. } else if (newStartIdx > newEndIdx) {
  66. removeVnodes(oldCh, oldStartIdx, oldEndIdx);
  67. }
  68. }

直接看源码可能比较难以捋清其中的关系,我们通过图来看一下。
VNode-基础原理 - 图1
首先,在新老两个VNode节点的左右头尾两侧都有一个变量标记,在遍历过程中这几个变量都会向中间靠拢。当oldStartIdx > oldEndIdx或者newStartIdx > newEndIdx时结束循环。
索引与VNode节点的对应关系: oldStartIdx => oldStartVnode oldEndIdx => oldEndVnode newStartIdx => newStartVnode newEndIdx => newEndVnode
在遍历中,如果存在key,并且满足sameVnode,会将该DOM节点进行复用,否则则会创建一个新的DOM节点。
首先,oldStartVnode、oldEndVnode与newStartVnode、newEndVnode两两比较一共有2*2=4种比较方法。
当新老VNode节点的start或者end满足sameVnode时,也就是sameVnode(oldStartVnode, newStartVnode)或者sameVnode(oldEndVnode, newEndVnode),直接将该VNode节点进行patchVnode即可。
VNode-基础原理 - 图2
如果oldStartVnode与newEndVnode满足sameVnode,即sameVnode(oldStartVnode, newEndVnode)。
这时候说明oldStartVnode已经跑到了oldEndVnode后面去了,进行patchVnode的同时还需要将真实DOM节点移动到oldEndVnode的后面。
VNode-基础原理 - 图3
如果oldEndVnode与newStartVnode满足sameVnode,即sameVnode(oldEndVnode, newStartVnode)。
这说明oldEndVnode跑到了oldStartVnode的前面,进行patchVnode的同时真实的DOM节点移动到了oldStartVnode的前面。
VNode-基础原理 - 图4
如果以上情况均不符合,则通过createKeyToOldIdx会得到一个oldKeyToIdx,里面存放了一个key为旧的VNode,value为对应index序列的哈希表。从这个哈希表中可以找到是否有与newStartVnode一致key的旧的VNode节点,如果同时满足sameVnode,patchVnode的同时会将这个真实DOM(elmToMove)移动到oldStartVnode对应的真实DOM的前面。
VNode-基础原理 - 图5
当然也有可能newStartVnode在旧的VNode节点找不到一致的key,或者是即便key相同却不是sameVnode,这个时候会调用createElm创建一个新的DOM节点。
VNode-基础原理 - 图6
到这里循环已经结束了,那么剩下我们还需要处理多余或者不够的真实DOM节点。
1.当结束时oldStartIdx > oldEndIdx,这个时候老的VNode节点已经遍历完了,但是新的节点还没有。说明了新的VNode节点实际上比老的VNode节点多,也就是比真实DOM多,需要将剩下的(也就是新增的)VNode节点插入到真实DOM节点中去,此时调用addVnodes(批量调用createElm的接口将这些节点加入到真实DOM中去)。
VNode-基础原理 - 图7
2。同理,当newStartIdx > newEndIdx时,新的VNode节点已经遍历完了,但是老的节点还有剩余,说明真实DOM节点多余了,需要从文档中删除,这时候调用removeVnodes将这些多余的真实DOM删除。
VNode-基础原理 - 图8