一些生命周期初探,源码学习陆

By admin in 4858美高梅 on 2019年4月5日

踏入前端,步入玄学

virtual-dom(后文简称vdom)的定义大规模的加大依然得益于react出现,virtual-dom也是react那一个框架的可怜重大的表征之1。比较于频繁的手动去操作dom而带来质量难点,vdom很好的将dom做了一层映射关系,进而将在我们本须求平素开始展览dom的一名目繁多操作,映射到了操作vdom,而vdom上定义了有关真实dom的片段要害的新闻,vdom完全是用js去贯彻,和宿主浏览器未有其他关系,其它得益于js的实践进程,将原先供给在实事求是dom举办的开创节点,删除节点,添加节点等1多元复杂的dom操作全部停放vdom中展开,那样就透过操作vdom来增强直接操作的dom的功能和性子。

初六和家眷出去玩,没写完博客。跳票了~

转发请声明出处
正文转发至自家的blog

 
  一七年终至1八年底附带做了vue的有的框架搭建,中途断断续续用了有的vue,时隔多少个月后的干活又拾起vue,对于部分原理性的知识淡忘了,正值那段时日利用中相见了一些坑,又拨了部分代码出来温习温习。

一些生命周期初探,源码学习陆。Vue在贰.0本子也引入了vdom。其vdom算法是依据snabbdom算法所做的修改。

所谓虚拟DOM,是一个用于表示真实 DOM 结构和天性的 JavaScript
对象,那个指标用于相比较虚拟 DOM 和当前真正 DOM
的差异化,然后开始展览部分渲染从而达成性能上的优化。在Vue.js 中虚构 DOM 的
JavaScript 对象正是 VNode。
接下去我们一步步剖析:

目录

  • 前言
  • virtual dom
  • 分析diff
  • 总结

      正式进入大前端,新同事称谓的玄学…

在Vue的满贯应用生命周期个中,每一回要求更新视图的时候便会利用vdom。那么在Vue当中,vdom是什么和Vue这几个框架融合在同步工作的吧?以及我们日常提到的vdom的diff算法又是哪些的啊?接下去就通过那篇小说简单的向大家介绍下Vue个中的vdom是怎么样去做事的。

VNode 是什么?


既是是虚构 DOM 的效能是转为真实的
DOM,那那正是三个渲染的经过。所以大家看看 render
方法。在前头的就学中我们明白了,vue 的渲染函数 _render
方法重返的就是三个 VNode 对象。而在 initRender
开首化渲染的章程中定义的 vm._cvm.$createElement
方法中,createElement 最终也是回来 VNode 对象。所以 VNode
是渲染的关键所在。
话不多说,来探望那几个VNode为什么方神圣。

// src/core/vdom/vnode.js
export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node

  // strictly internal
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?
  asyncFactory: Function | void; // async component factory function
  asyncMeta: Object | void;
  isAsyncPlaceholder: boolean;
  ssrContext: Object | void;
  fnContext: Component | void; // real context vm for functional nodes
  fnOptions: ?ComponentOptions; // for SSR caching
  fnScopeId: ?string; // functioanl scope id support

  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    this.tag = tag // 当前节点标签名
    this.data = data // 当前节点数据(VNodeData类型)
    this.children = children // 当前节点子节点
    this.text = text // 当前节点文本
    this.elm = elm // 当前节点对应的真实DOM节点
    this.ns = undefined // 当前节点命名空间
    this.context = context // 当前节点上下文
    this.fnContext = undefined // 函数化组件上下文
    this.fnOptions = undefined // 函数化组件配置项
    this.fnScopeId = undefined // 函数化组件ScopeId
    this.key = data && data.key // 子节点key属性
    this.componentOptions = componentOptions // 组件配置项 
    this.componentInstance = undefined // 组件实例
    this.parent = undefined // 当前节点父节点
    this.raw = false // 是否为原生HTML或只是普通文本
    this.isStatic = false // 静态节点标志 keep-alive
    this.isRootInsert = true // 是否作为根节点插入
    this.isComment = false // 是否为注释节点
    this.isCloned = false // 是否为克隆节点
    this.isOnce = false // 是否为v-once节点
    this.asyncFactory = asyncFactory // 异步工厂方法 
    this.asyncMeta = undefined // 异步Meta
    this.isAsyncPlaceholder = false // 是否为异步占位
  }

  // 容器实例向后兼容的别名
  get child (): Component | void {
    return this.componentInstance
  }
}

实际便是3个平常的 JavaScript Class 类,中间有各样数码用于描述虚拟
DOM,上边用三个事例来看望VNode 是怎么着彰显 DOM 的。

    <div id="app">
        {{ message }}
        <ul>
            <li v-for="item of list" class="item-cls">{{ item }}</li>
        </ul>
    </div>

    <script>
        new Vue({
            el: '#app',
            data: {
                message: 'hello Vue.js',
                list: ['jack', 'rose', 'james']
            }
        })
    </script>

那几个事例最终结果如图:

4858美高梅 1

HTML突显结果

简化后的VNode对象结果如图:

{
    "tag": "div",
    "data": {
        "attr": { "id": "app" }
    },
    "children": [
        {
            "tag": "span",
            "children": [
                { "text": "hello Vue.js" }
            ]
        },
        {
            "tag": "ul",
            "children": [
                {
                    "tag": "li",
                    "data": { "staticClass": "item-cls" },
                    "children": [
                        { "text": "jack" }
                    ]
                },
                {
                    "tag": "li",
                    "data": { "staticClass": "item-cls" },
                    "children": [
                        { "text": "rose" }
                    ]
                },
                {
                    "tag": "li",
                    "data": { "staticClass": "item-cls" },
                    "children": [
                        { "text": "james" }
                    ]
                }
            ]
        }
    ],
    "context": "$Vue$3",
    "elm": "div#app"
}

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

  • 有着目的的 context 选项都指向了 Vue 实例。
  • elm 属性则针对了其相呼应的实际 DOM 节点。
  • DOM 中的文本内容被视作了二个唯有 text 没有 tag 的节点。
  • 像 class、id 等HTML属性都位于了 data

咱俩询问了VNode 是怎么着描述 DOM 之后,来读书怎么将虚拟
DOM 变为真正的 DOM。

前言

vue二.0加盟了virtual
dom,有向react靠拢的意趣。vue的diff位于patch.js文本中,作者的1个小框架aoy也同样应用此算法,该算法来源于snabbdom,复杂度为O(n)。
叩问diff进程可以让大家更敏捷的施用框架。
正文力求以活跃的主意来证明那一个diff的历程。

读在最前面:

首先,大家依然来看下Vue生命周期当中开首化的最后阶段:将vm实例挂载到dom上,源码在src/core/instance

patch —— Virtual DOM 的核心


在此从前边的篇章中得以清楚,Vue的渲染进度(无论是初阶化视图依旧更新视图)最终都将走到
_update 方法中,再来看看这一个 _update 方法。

  // src/core/instance/lifecycle.js
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const prevActiveInstance = activeInstance
    activeInstance = vm
    vm._vnode = vnode

    if (!prevVnode) {
      // 初始化渲染
      vm.$el = vm.__patch__(
        vm.$el, vnode, hydrating, false /* removeOnly */,
        vm.$options._parentElm,
        vm.$options._refElm
      )
      // no need for the ref nodes after initial patch
      // this prevents keeping a detached DOM tree in memory (#5851)
      vm.$options._parentElm = vm.$options._refElm = null
    } else {
      // 更新渲染
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    activeInstance = prevActiveInstance
    // update __vue__ reference
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook.
  }

简易发现更新试图都以运用了 vm.__patch__ 方法,大家继承往下跟。

// src/platforms/web/runtime/index.js
Vue.prototype.__patch__ = inBrowser ? patch : noop

此间啰嗦一句,要找vue的大局方法,如 vm.aaa ,直接查找
Vue.prototype.aaa 即可。
继续找下去:

// src/platforms/web/runtime/patch.js
export const patch: Function = createPatchFunction({ nodeOps, modules })

找到 createPatchFunction 方法~

// src/core/vdom/patch.js
export function createPatchFunction (backend) {
  ……
  return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
    // 当前 VNode 未定义、老的 VNode 定义了,调用销毁钩子。
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) {
      // 老的 VNode 未定义,初始化。
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue, parentElm, refElm)
    } else {
      // 当前 VNode 和老 VNode 都定义了,执行更新操作
      // DOM 的 nodeType http://www.w3school.com.cn/jsref/prop_node_nodetype.asp
      const isRealElement = isDef(oldVnode.nodeType) // 是否为真实 DOM 元素
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        // 修改已有根节点
        patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
      } else {
        // 已有真实 DOM 元素,处理 oldVnode
        if (isRealElement) {
          // 挂载一个真实元素,确认是否为服务器渲染环境或者是否可以执行成功的合并到真实 DOM 中
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          if (isTrue(hydrating)) {
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              // 调用 insert 钩子
              // inserted:被绑定元素插入父节点时调用 
              invokeInsertHook(vnode, insertedVnodeQueue, true)
              return oldVnode
            }
          }
          // 不是服务器渲染或者合并到真实 DOM 失败,创建一个空节点替换原有节点
          oldVnode = emptyNodeAt(oldVnode)
        }

        // 替换已有元素
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)

        // 创建新节点
        createElm(
          vnode,
          insertedVnodeQueue,
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )

        // 递归更新父级占位节点元素,
        if (isDef(vnode.parent)) {
          let ancestor = vnode.parent
          const patchable = isPatchable(vnode)
          while (ancestor) {
            for (let i = 0; i < cbs.destroy.length; ++i) {
              cbs.destroy[i](ancestor)
            }
            ancestor.elm = vnode.elm
            if (patchable) {
              for (let i = 0; i < cbs.create.length; ++i) {
                cbs.create[i](emptyNode, ancestor)
              }
              const insert = ancestor.data.hook.insert
              if (insert.merged) {
                for (let i = 1; i < insert.fns.length; i++) {
                  insert.fns[i]()
                }
              }
            } else {
              registerRef(ancestor)
            }
            ancestor = ancestor.parent
          }
        }

        // 销毁旧节点
        if (isDef(parentElm)) {
          removeVnodes(parentElm, [oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }
    // 调用 insert 钩子
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }
}

切切实实分析看代码注释~抛开调用生命周期钩子和销毁就节点不谈,大家发现代码中的关键在于
createElmpatchVnode 方法。

virtual dom

若果不领会virtual
dom,要精通diff的经过是相比劳累的。虚拟dom对应的是实在dom,
使用document.CreateElement
document.CreateTextNode创制的正是真心真意节点。

大家得以做个试验。打字与印刷出三个空成分的率先层属性,能够看到规范让要素完成的东西太多了。借使每一回都重新生成新的因素,对品质是远大的荒废。

var mydiv = document.createElement('div');
for(var k in mydiv ){
  console.log(k)
}

virtual dom正是斩草除根那个题材的3个思路,到底什么是virtual
dom呢?通俗易懂的来说正是用二个简单易行的指标去替代复杂的dom对象。
举个大致的例证,我们在body里插入二个class为a的div。

var mydiv = document.createElement('div');
mydiv.className = 'a';
document.body.appendChild(mydiv);

对于这么些div我们可以用三个大约的对象mydivVirtual表示它,它存款和储蓄了对应dom的一些生死攸关参数,在转移dom在此以前,会先相比相应虚拟dom的数额,假使急需改变,才会将改变使用到实际dom上。

//伪代码
var mydivVirtual = { 
  tagName: 'DIV',
  className: 'a'
};
var newmydivVirtual = {
   tagName: 'DIV',
   className: 'b'
}
if(mydivVirtual.tagName !== newmydivVirtual.tagName || mydivVirtual.className  !== newmydivVirtual.className){
   change(mydiv)
}

// 会执行相应的修改 mydiv.className = 'b';
//最后  <div class='b'></div>

  1、本文依据难题,讲述差不多 Vue虚拟Dom Diff
思路、数据响应式机制相关,源码版本 
Vue.js v2.5.17-beta.0

  Vue.prototype._init = function () {
    ...
    vm.$mount(vm.$options.el) // 实际上是调用了mountComponent方法
    ...
  }  

createElm

先看 createElm 方法,这几个法子成立了诚实 DOM 成分。

  function createElm (
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
  ) {
    if (isDef(vnode.elm) && isDef(ownerArray)) {
      vnode = ownerArray[index] = cloneVNode(vnode)
    }

    vnode.isRootInsert = !nested // for transition enter check
    // 创建组件
    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)
      setScope(vnode)

      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)
    }
  }

根本关心代码中的方法执行。代码太多,就不贴出来了,不难说说用途。

  • 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: 向成分添加新的子节点,作为最终1个子节点。
  • insertBefore: 在钦命的已有的子节点以前插入新节点。
  • tagName: 再次回到成分的标签名。
  • removeChild: 从要素中移除子节点。
  • createElementNS: 创建带有钦定命名空间的要秋天点。
  • createElement: 创立成分节点。
  • createComment: 创制注释节点。
  • createTextNode: 成立文本节点。
  • setAttribute: 把内定属性设置或改动为钦定值。
  • nextSibling: 重临位于同一节点树层级的下3个节点。
  • parentNode: 重回成分父节点。
  • setTextContent:
    获取文本内容(那个未在w叁school中找到,然而相应正是其一意思了)。

OK,知道以上措施就比较好精晓了,createElm 方法的末梢指标就是成立真实的
DOM 对象。

读到那里就会发生3个疑点,为何不直接修改dom而必要加1层virtual dom呢?

不少时候手工业优化dom确实会比virtual
dom功用高,对于比较简单的dom结构用手工业优化没极度,但当页面结构很巨大,结构很复杂时,手工业优化会花去多量时间,而且可维护性也不高,无法确定保障每种人都有手工业优化的力量。至此,virtual
dom的缓解方案出现,virtual
dom很多时候都不是最优的操作,但它抱有普适性,在作用、可维护性之间达平衡。

virtual dom
另二个重马虎义正是提供三当中间层,js去写ui,ios安卓之类的负担渲染,就像是reactNative一样。

       二、知识点:vue virtual dom diff、 observe 、 watch、render
,各知识点暂不深远剖析

mountComponent函数的定义是:

patchVnode

看过了创办真实 DOM 后,大家来上学虚拟 DOM 怎么样落到实处 DOM
的立异。那才是杜撰 DOM 的留存意义 —— 比对并部分更新 DOM
以高达品质优化的指标。
看代码~

  // 补丁 vnode
  function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    // 新旧 vnode 相等
    if (oldVnode === vnode) {
      return
    }

    const elm = vnode.elm = oldVnode.elm
    // 异步占位
    if (isTrue(oldVnode.isAsyncPlaceholder)) {
      if (isDef(vnode.asyncFactory.resolved)) {
        hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
      } else {
        vnode.isAsyncPlaceholder = true
      }
      return
    }

    // 如果新旧 vnode 为静态;新旧 vnode key相同;
    // 新 vnode 是克隆所得;新 vnode 有 v-once 的属性
    // 则新 vnode 的 componentInstance 用老的 vnode 的。
    // 即 vnode 的 componentInstance 保持不变。
    if (isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
      vnode.componentInstance = oldVnode.componentInstance
      return
    }

    let i
    const data = vnode.data
    // 执行 data.hook.prepatch 钩子。
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode)
    }

    const oldCh = oldVnode.children
    const ch = vnode.children
    if (isDef(data) && isPatchable(vnode)) {
      // 遍历 cbs,执行 update 方法
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      // 执行 data.hook.update 钩子
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    // 旧 vnode 的 text 选项为 undefined
    if (isUndef(vnode.text)) {
      if (isDef(oldCh) && isDef(ch)) {
        // 新旧 vnode 都有 children,且不同,执行 updateChildren 方法。
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
        // 清空文本,添加 vnode
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        // 移除 vnode
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        // 如果新旧 vnode 都是 undefined,清空文本
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      // 有不同文本内容,更新文本内容
      nodeOps.setTextContent(elm, vnode.text)
    }
    if (isDef(data)) {
      // 执行 data.hook.postpatch 钩子,表明 patch 完毕
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

源码中添加了部分证明便于精通,来理一下逻辑。

  1. 万壹八个vnode相等,不须要 patch,退出。
  2. 若果是异步占位,执行 hydrate 方法或然定义 isAsyncPlaceholder 为
    true,然后退出。
  3. 假使八个vnode都为静态,不用更新,所以讲之前的 componentInstance
    实例传给当前 vnode,并退出。
  4. 执行 prepatch 钩子。
  5. 遍历调用 update 回调,并实施 update 钩子。
  6. 要是七个 vnode 都有 children,且 vnode 未有 text、三个 vnode
    不对等,执行 updateChildren 方法。那是虚构 DOM 的首要性。
  7. 若果新 vnode 有 children,而老的从未有过,清空文本,并丰盛 vnode 节点。
  8. 借使老 vnode 有 children,而新的没哟,清空文本,并移除 vnode 节点。
  9. 固然多少个 vnode 都未曾 children,老 vnode 有 text ,新 vnode 未有text ,则清空 DOM 文本内容。
  10. 若是老 vnode 和新 vnode 的 text 分化,更新 DOM 成分文本内容。
  11. 调用 postpatch 钩子。

其中,addVnodes 方法和 removeVnodes
都相比较不难,很好通晓。那里我们来探望关键代码 updateChildren 方法。

  function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    let oldStartIdx = 0
    let newStartIdx = 0
    let oldEndIdx = oldCh.length - 1
    let oldStartVnode = oldCh[0]
    let oldEndVnode = oldCh[oldEndIdx]
    let newEndIdx = newCh.length - 1
    let newStartVnode = newCh[0]
    let newEndVnode = newCh[newEndIdx]
    let oldKeyToIdx, idxInOld, vnodeToMove, refElm

    // removeOnly 是一个只用于 <transition-group> 的特殊标签,
    // 确保移除元素过程中保持一个正确的相对位置。
    const canMove = !removeOnly

    if (process.env.NODE_ENV !== 'production') {
      checkDuplicateKeys(newCh)
    }

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        // 开始老 vnode 向右一位
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        // 结束老 vnode 向左一位
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        // 新旧开始 vnode 相似,进行pacth。开始 vnode 向右一位
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // 新旧结束 vnode 相似,进行patch。结束 vnode 向左一位
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        // 新结束 vnode 和老开始 vnode 相似,进行patch。
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
        // 老开始 vnode 插入到真实 DOM 中,老开始 vnode 向右一位,新结束 vnode 向左一位
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        // 老结束 vnode 和新开始 vnode 相似,进行 patch。
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        // 老结束 vnode 插入到真实 DOM 中,老结束 vnode 向左一位,新开始 vnode 向右一位
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        // 获取老 Idx 的 key
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        // 给老 idx 赋值
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        if (isUndef(idxInOld)) {
          // 如果老 idx 为 undefined,说明没有这个元素,创建新 DOM 元素。
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          // 获取 vnode
          vnodeToMove = oldCh[idxInOld]
          if (sameVnode(vnodeToMove, newStartVnode)) {
            // 如果生成的 vnode 和新开始 vnode 相似,执行 patch。
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
            // 赋值 undefined,插入 vnodeToMove 元素
            oldCh[idxInOld] = undefined
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // 相同的key不同的元素,视为新元素
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        // 新开始 vnode 向右一位
        newStartVnode = newCh[++newStartIdx]
      }
    }
    // 如果老开始 idx 大于老结束 idx,如果是有效数据则添加 vnode 到新 vnode 中。
    if (oldStartIdx > oldEndIdx) {
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
      // 移除 vnode
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
  }

意味着已看晕……让大家慢慢捋1捋……

  1. 看参数,当中 oldCh 和 newCh 即表示了新旧 vnode
    数组,两组数组通过比对的章程来差别化更新 DOM。
  2. 概念了壹些变量:起初索引值、停止索引值、开首vnode、截至vnode等等……
  3. 拓展巡回遍历,遍历条件为 oldStartIdx <= oldEndIdx 和 newStartIdx
    <= newEndIdx,在遍历过程中,oldStartIdx 和 newStartIdx
    递增,oldEndIdx 和 newEndIdx 递减。当条件不符合跳出遍历循环。
  4. 如果 oldStartVnode 和 newStartVnode 相似,执行 patch。
![](https://upload-images.jianshu.io/upload_images/1987062-3c53cb4442d3fc58)

image
  1. 如果 oldEndVnode 和 newEndVnode 相似,执行 patch。
  2. 假若 oldStartVnode 和 newEndVnode 相似,执行
    patch,并且将该节点移动到 vnode 数组末一人。
![](https://upload-images.jianshu.io/upload_images/1987062-0b47f3cb7f762873)

image
  1. 假设 oldEndVnode 和 newStartVnode 相似,执行
    patch,并且将该节点移动到 vnode 数组第3人。
![](https://upload-images.jianshu.io/upload_images/1987062-f6203babe1e15791)

image
  1. 若是未有一样的 idx,执行 createElm 方法创设成分。
  2. 一经如有相同的 idx,假诺五个 vnode 相似,执行
    patch,并且将该节点移动到 vnode 数组第3人。借使两个 vnode
    不1般,视为新元素,执行 createElm 创设。
![](https://upload-images.jianshu.io/upload_images/1987062-2a6b908889782ac4)

image
  1. 设若老 vnode 数组的开首索引大于甘休索引,表明新 node 数CEO度超越老
    vnode 数组,执行 addVnodes 方法添加那几个新 vnode 到 DOM 中。
![](https://upload-images.jianshu.io/upload_images/1987062-d353c99c30bb5f25)

image
  1. 借使老 vnode 数组的早先索引小于结束索引,表达老 node 数总裁度超越新
    vnode 数组,执行 removeVnodes 方法从 DOM 中移除老 vnode 数组中多余的
    vnode。
![](https://upload-images.jianshu.io/upload_images/1987062-c8aa456d7f2839da)

image

啊……就是那样!

分析diff

壹篇分外经典的文章React’s diff
algorithm中的图,react的diff其实和vue的diff焦作小异。所以这张图能很好的分解进度。比较只会在同层级举办,
不会跨层级相比。

4858美高梅 2

举个形象的事例。

<!-- 之前 -->
<div>           <!-- 层级1 -->
  <p>            <!-- 层级2 -->
    <b> aoy </b>   <!-- 层级3 -->   
    diff</Span>
  </P> 
</div>

<!-- 之后 -->
<div>            <!-- 层级1 -->
  <p>             <!-- 层级2 -->
      <b> aoy </b>        <!-- 层级3 -->
  </p>
  diff</Span>
</div>

我们兴许希望将直接移动到`<p>`的后边,这是最优的操作。但是实际的diff操作是移除`<p>`里的在创制二个新的插到`<p>`的后边。
因为新加的
在层级二,旧的在层级三,属于差别层级的可比。

  三、阅读本文,读者应精晓Vue有肯定的前端基础,知道有个别名词概念如:render、vnode、virtual
dom 

export function mountComponent (
 vm: Component,
 el: ?Element,
 hydrating?: boolean
): Component {
 // vm.$el为真实的node
 vm.$el = el
 // 如果vm上没有挂载render函数
 if (!vm.$options.render) {
  // 空节点
  vm.$options.render = createEmptyVNode
 }
 // 钩子函数
 callHook(vm, 'beforeMount')

 let updateComponent
 /* istanbul ignore if */
 if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
  ...
 } else {
  // updateComponent为监听函数, new Watcher(vm, updateComponent, noop)
  updateComponent = () => {
   // Vue.prototype._render 渲染函数
   // vm._render() 返回一个VNode
   // 更新dom
   // vm._render()调用render函数,会返回一个VNode,在生成VNode的过程中,会动态计算getter,同时推入到dep里面
   vm._update(vm._render(), hydrating)
  }
 }

 // 新建一个_watcher对象
 // vm实例上挂载的_watcher主要是为了更新DOM
 // vm/expression/cb
 vm._watcher = new Watcher(vm, updateComponent, noop)
 hydrating = false

 // manually mounted instance, call mounted on self
 // mounted is called for render-created child components in its inserted hook
 if (vm.$vnode == null) {
  vm._isMounted = true
  callHook(vm, 'mounted')
 }
 return vm
}

最后

终究是Vue的中坚作用之一,即便简易了成千上万代码,但博客篇幅十分短。写了两日才写完。可是写完博客后觉得对于
Vue 的精晓又加深了诸多。
在下一篇博客中,大家1道来上学template的剖析。

源码分析

文中的代码位于aoy-diff中,已经精简了重重代码,留下最大旨的有个别。

diff的长河正是调用patch函数,就好像打补丁一样修改真实dom。

function patch (oldVnode, vnode) {
    if (sameVnode(oldVnode, vnode)) {
        patchVnode(oldVnode, vnode)
    } else {
        const oEl = oldVnode.el
        let parentEle = api.parentNode(oEl)
        createEle(vnode)
        if (parentEle !== null) {
            api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl))
            api.removeChild(parentEle, oldVnode.el)
            oldVnode = null
        }
    }
    return vnode
}

patch函数有四个参数,vnodeoldVnode,也正是新旧八个虚拟节点。在那在此以前,大家先理解完整的vnode都有何性质,举个1个大约的例证:

// body下的 <div id="v" class="classA"><div> 对应的 oldVnode 就是

{
  el:  div  //对真实的节点的引用,本例中就是document.querySelector('#id.classA')
  tagName: 'DIV',   //节点的标签
  sel: 'div#v.classA'  //节点的选择器
  data: null,       // 一个存储节点属性的对象,对应节点的el[prop]属性,例如onclick , style
  children: [], //存储子节点的数组,每个子节点也是vnode结构
  text: null,    //如果是文本节点,对应文本节点的textContent,否则为null
}

要求小心的是,el属性引用的是此 virtual
dom对应的真正dom,patchvnode参数的el最初是null,因为patch此前它还未曾对号入座的真实性dom。

来到patch的首先有些,

if (sameVnode(oldVnode, vnode)) {
    patchVnode(oldVnode, vnode)
} 

sameVnode函数正是看那五个节点是否值得相比,代码分外简单:

function sameVnode(oldVnode, vnode){
    return vnode.key === oldVnode.key && vnode.sel === oldVnode.sel
}

七个vnode的key和sel相同才去比较它们,比如pspandiv.classAdiv.classB都被认为是不相同结构而不去比较它们。

假如值得相比会实施patchVnode(oldVnode, vnode),稍后会详细讲patchVnode函数。

当节点不值得相比,进入else中

else {
        const oEl = oldVnode.el
        let parentEle = api.parentNode(oEl)
        createEle(vnode)
        if (parentEle !== null) {
            api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl))
            api.removeChild(parentEle, oldVnode.el)
            oldVnode = null
        }
    }

进程如下:

  • 取得oldvnode.el的父节点,parentEle是真实dom
  • createEle(vnode)会为vnode始建它的实际dom,令vnode.el
    =真实dom
  • parentEle将新的dom插入,移除旧的dom
    当不值得相比较时,新节点直接把老节点整个替换了

最后

return vnode

patch最后会重回vnode,vnode和进入patch以前的不一致在哪?
没错,就是vnode.el,唯一的变动正是事先vnode.el = null,
而未来它引用的是呼应的真人真事dom。

var oldVnode = patch (oldVnode, vnode)

于今完结贰个patch进度。

  4、本文先引出难点及原理,然后单用3个完毕给出详情解析

瞩目上边的代码中定义了多个updateComponent函数,那几个函数执行的时候内部会调用vm._update(vm._render(),
hyddrating)方法,其中vm._render方法会重返八个新的vnode,(关于vm_render是何许生成vnode的建议大家看看vue的有关compile阶段的代码),然后传入vm._update方法后,就用那几个新的vnode和老的vnode进行diff,最终形成dom的换代工作。那么updateComponent都以在怎么样时候去进行调用呢?

参照文书档案

  • Vue官网
  • VirtualDOM与diff(Vue实现)
  • Vue二.一.7源码学习
  • w3school

patchVnode

四个节点值得相比时,会调用patchVnode函数

patchVnode (oldVnode, vnode) {
    const el = vnode.el = oldVnode.el
    let i, oldCh = oldVnode.children, ch = vnode.children
    if (oldVnode === vnode) return
    if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
        api.setTextContent(el, vnode.text)
    }else {
        updateEle(el, vnode, oldVnode)
        if (oldCh && ch && oldCh !== ch) {
            updateChildren(el, oldCh, ch)
        }else if (ch){
            createEle(vnode) //create el's children dom
        }else if (oldCh){
            api.removeChildren(el)
        }
    }
}

const el = vnode.el = oldVnode.el
那是很主要的一步,让vnode.el引用到明日的实在dom,当el修改时,vnode.el会联手变化。

节点的相比有5种情状

  1. if (oldVnode === vnode),他们的引用一致,可以认为并未有成形。

  2. if(oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text),文本节点的可比,须求修改,则会调用Node.textContent = vnode.text

  3. if( oldCh && ch && oldCh !== ch ),
    七个节点都有子节点,而且它们不壹致,那样我们会调用updateChildren函数相比子节点,那是diff的主干,前边会讲到。

  4. else if (ch),唯有新的节点有子节点,调用createEle(vnode)vnode.el业已引用了老的dom节点,createEle函数会在老dom节点上添加子节点。

  5. else if (oldCh),新节点没有子节点,老节点有子节点,直接删除老节点。

  4、v-if案例解析(element-ui 
form-item表单不可能印证难点剖析 )(问一)

vm._watcher = new Watcher(vm, updateComponent, noop)

Vue.js学习连串

是因为前端知识碎片化严重,笔者期望能够系统化的整理出一套关于Vue的上学体系博客。

updateChildren

updateChildren (parentElm, oldCh, newCh) {
    let oldStartIdx = 0, newStartIdx = 0
    let oldEndIdx = oldCh.length - 1
    let oldStartVnode = oldCh[0]
    let oldEndVnode = oldCh[oldEndIdx]
    let newEndIdx = newCh.length - 1
    let newStartVnode = newCh[0]
    let newEndVnode = newCh[newEndIdx]
    let oldKeyToIdx
    let idxInOld
    let elmToMove
    let before
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
            if (oldStartVnode == null) {   //对于vnode.key的比较,会把oldVnode = null
                oldStartVnode = oldCh[++oldStartIdx] 
            }else if (oldEndVnode == null) {
                oldEndVnode = oldCh[--oldEndIdx]
            }else if (newStartVnode == null) {
                newStartVnode = newCh[++newStartIdx]
            }else if (newEndVnode == null) {
                newEndVnode = newCh[--newEndIdx]
            }else if (sameVnode(oldStartVnode, newStartVnode)) {
                patchVnode(oldStartVnode, newStartVnode)
                oldStartVnode = oldCh[++oldStartIdx]
                newStartVnode = newCh[++newStartIdx]
            }else if (sameVnode(oldEndVnode, newEndVnode)) {
                patchVnode(oldEndVnode, newEndVnode)
                oldEndVnode = oldCh[--oldEndIdx]
                newEndVnode = newCh[--newEndIdx]
            }else if (sameVnode(oldStartVnode, newEndVnode)) {
                patchVnode(oldStartVnode, newEndVnode)
                api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
                oldStartVnode = oldCh[++oldStartIdx]
                newEndVnode = newCh[--newEndIdx]
            }else if (sameVnode(oldEndVnode, newStartVnode)) {
                patchVnode(oldEndVnode, newStartVnode)
                api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
                oldEndVnode = oldCh[--oldEndIdx]
                newStartVnode = newCh[++newStartIdx]
            }else {
               // 使用key时的比较
                if (oldKeyToIdx === undefined) {
                    oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 有key生成index表
                }
                idxInOld = oldKeyToIdx[newStartVnode.key]
                if (!idxInOld) {
                    api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
                    newStartVnode = newCh[++newStartIdx]
                }
                else {
                    elmToMove = oldCh[idxInOld]
                    if (elmToMove.sel !== newStartVnode.sel) {
                        api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
                    }else {
                        patchVnode(elmToMove, newStartVnode)
                        oldCh[idxInOld] = null
                        api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el)
                    }
                    newStartVnode = newCh[++newStartIdx]
                }
            }
        }
        if (oldStartIdx > oldEndIdx) {
            before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el
            addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
        }else if (newStartIdx > newEndIdx) {
            removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
        }
}

代码很密集,为了形象的叙述那一个进度,能够看看那张图。

<div align=“center”>

4858美高梅 3

</div>

进度能够包罗为:oldChnewCh各有三个头尾的变量StartIdxEndIdx,它们的1个变量互相比较,壹共有四种相比较艺术。假诺4种比较都没相称,假若设置了key,就会用key举行相比,在相比的经过中,变量会往中间靠,1旦StartIdx>EndIdx表明oldChnewCh至少有三个早就遍历完了,就会截止比较。

  5、watch案例解析(element-ui el-select
不恐怕选中难点浅析)(问二)

实例化三个watcher,在求值的历程中this.value = this.lazy ? undefined :
this.get(),会调用this.get()方法,因而在实例化的进度个中Dep.target会被设为这么些watcher,通过调用vm._render()方法生成新的Vnode并举办diff的经过中成功了模版当中变量注重收集工作。即那么些watcher被添加到了在模板个中所绑定变量的注重性当中。一旦model中的响应式的数据产生了转移,这一个响应式的数量所保险的dep数组便会调用dep.notify()方法成功具有依赖遍历执行的做事,那其间就包罗了视图的立异即updateComponent方法的调用。

Vue.js学习种类项目地址

正文源码已入账到GitHub中,以供参考,当然能留下多个star越来越好啊
https://github.com/violetjack/VueStudyDemos

具体的diff分析

安装key和不安装key的差别:
不设key,newCh和oldCh只会进行头尾两端的相互相比较,设key后,除了头尾两端的相比外,还会从用key生成的对象oldKeyToIdx中查找匹配的节点,所以为节点设置key能够越来越高效的行使dom。

diff的遍历进程中,只固然对dom举行的操作都调用api.insertBeforeapi.insertBefore只是原生insertBefore的简便封装。
比较分为三种,壹种是有vnode.key的,壹种是未曾的。但那二种相比对真实dom的操作是如出1辙的。

对于与sameVnode(oldStartVnode, newStartVnode)sameVnode(oldEndVnode,newEndVnode)为true的地方,不要求对dom进行活动。

小结遍历进程,有3种dom操作:

  1. oldStartVnodenewEndVnode值得相比,说明oldStartVnode.el跑到oldEndVnode.el的末尾了。

图中若是startIdx遍历到1。

4858美高梅 4

  1. oldEndVnodenewStartVnode值得相比,表明
    oldEndVnode.el跑到了newStartVnode.el的前边。

4858美高梅 5

  1. newCh中的节点oldCh里未有, 将新节点插入到oldStartVnode.el的前边。

4858美高梅 6

在完工时,分为二种状态:

  1. oldStartIdx > oldEndIdx,能够认为oldCh先遍历完。当然也有十分大可能率newCh此刻也恰恰完毕了遍历,统一都归为此类。此时newStartIdxnewEndIdx中间的vnode是骤增的,调用addVnodes,把她们整个插进before的后边,before诸多时候是为null的。addVnodes调用的是insertBefore操作dom节点,咱们看看insertBefore的文档:parentElement.insertBefore(newElement, referenceElement)
    1经referenceElement为null则newElement将被插入到子节点的末尾。若是newElement已经在DOM树中,newElement首先会从DOM树中移除。所以before为null,newElement将被插入到子节点的最终。

4858美高梅 7

  1. newStartIdx > newEndIdx,能够认为newCh先遍历完。此时oldStartIdxoldEndIdx里头的vnode在新的子节点里早已不存在了,调用removeVnodes将它们从dom里删除。

4858美高梅 8

 

updateComponent方法的定义是:

有关作者

维尔莉特杰克,高效学习前端工程师,喜欢钻研升高功用的法子,也留意于Vue前端相关知识的就学、整理。
迎接关怀、点赞、评论留言~作者将不止涌出Vue相关优质内容。

和讯天涯论坛:
http://weibo.com/u/2640909603
掘金:https://gold.xitu.io/user/571d953d39b0570068145cd1
CSDN:
http://blog.csdn.net/violetjack0808
简书:
http://www.jianshu.com/users/54ae4af3a98d/latest\_articles
Github:
https://github.com/violetjack

下边举个例证,画出diff完整的历程,每一步dom的扭转都用区别颜色的线标出。

  1. a,b,c,d,e若是是陆个差异的因素,大家并未有安装key时,b未有复用,而是径直开立新的,删除旧的。

4858美高梅 9

  1. 当大家给陆个要素加上唯一key时,b获得了的复用。

4858美高梅 10

本条事例假设大家利用手工业优化,只须求叁步就足以高达。

直白甩疑问案例,Fire!

updateComponent = () => {
 vm._update(vm._render(), hydrating)
}

总结

  • 尽大概不要跨层级的改动dom
  • 设置key能够最大化的施用节点
  • 绝不盲目相信diff的频率,在须求时得以手工业优化

4858美高梅,问一:当v-if为true时,会再也渲染相关dom节点吧?

姣好视图的更新工作实际正是调用了vm._update方法,那几个方法接收的率先个参数是刚生成的Vnode,调用的vm._update方法的概念是

难题源于(element-ui 
form-item表单不能印证难题浅析)

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this
  if (vm._isMounted) {
   callHook(vm, 'beforeUpdate')
  }
  const prevEl = vm.$el
  const prevVnode = vm._vnode
  const prevActiveInstance = activeInstance
  activeInstance = vm
  // 新的vnode
  vm._vnode = vnode
  // Vue.prototype.__patch__ is injected in entry points
  // based on the rendering backend used.
  // 如果需要diff的prevVnode不存在,那么就用新的vnode创建一个真实dom节点
  if (!prevVnode) {
   // initial render
   // 第一个参数为真实的node节点
   vm.$el = vm.__patch__(
    vm.$el, vnode, hydrating, false /* removeOnly */,
    vm.$options._parentElm,
    vm.$options._refElm
   )
  } else {
   // updates
   // 如果需要diff的prevVnode存在,那么首先对prevVnode和vnode进行diff,并将需要的更新的dom操作已patch的形式打到prevVnode上,并完成真实dom的更新工作
   vm.$el = vm.__patch__(prevVnode, vnode)
  }
  activeInstance = prevActiveInstance
  // update __vue__ reference
  if (prevEl) {
   prevEl.__vue__ = null
  }
  if (vm.$el) {
   vm.$el.__vue__ = vm
  }
  // if parent is an HOC, update its $el as well
  if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
   vm.$parent.$el = vm.$el
  }
}
<child v-if="true"><child>

在那一个格局当中最为首要的就是vm.__patch__措施,那也是1体virtaul-dom在那之中最为大旨的点子,首要形成了prevVnode和vnode的diff进程并基于必要操作的vdom节点打patch,最后生成新的实在dom节点并做到视图的换代工作。

答:会!(什么??? 难道v-if
为true不该直接坚强到结尾吧,是怎么让他这么微弱)

接下去就让我们看下vm.__patch__当中到底发生了怎么:

涉及 知识点 vue virtual dom diff 

  function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
    // 当oldVnode不存在时
    if (isUndef(oldVnode)) {
      // 创建新的节点
      createElm(vnode, insertedVnodeQueue, parentElm, refElm)
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
      // patch existing root node
      // 对oldVnode和vnode进行diff,并对oldVnode打patch
      patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
   } 
    }
  }

难题原理:

在对oldVnode和vnode类型判断中有个sameVnode方法,那个方式决定了是或不是供给对oldVnode和vnode举行diff及patch的长河。

序言:vue2引进了虚拟dom,即只要使用.vue的模版形式书写,会率先编写翻译为render函数,生成vnode,每一次数据变动后,会在nexttick中开始展览新老vnode比较进行patch操作

function sameVnode (a, b) {
 return (
  a.key === b.key &&
  a.tag === b.tag &&
  a.isComment === b.isComment &&
  isDef(a.data) === isDef(b.data) &&
  sameInputType(a, b)
 )
}

1、相比较前后 vnode对象相同颜色的节点开始展览同层级比较 ,如下图

sameVnode会对传播的一个vnode实行着力性能的相比较,唯有当基性格情相同的事态下才觉得那些二个vnode只是有些产生了更新,然后才会对那三个vnode实行diff,假诺二个vnode的基本性子存在分歧等的情景,那么就会直接跳过diff的进度,进而依照vnode新建1个真真的dom,同时删除老的dom节点。

 4858美高梅 11

vnode基本特性的定义能够瞻仰源码:src/vdom/vnode.js里面对于vnode的概念。

 

constructor (
  tag?: string,
  data?: VNodeData,     // 关于这个节点的data值,包括attrs,style,hook等
  children?: ?Array<VNode>, // 子vdom节点
  text?: string,    // 文本内容
  elm?: Node,      // 真实的dom节点
  context?: Component, // 创建这个vdom的上下文
  componentOptions?: VNodeComponentOptions
 ) {
  this.tag = tag
  this.data = data
  this.children = children
  this.text = text
  this.elm = elm
  this.ns = undefined
  this.context = context
  this.functionalContext = undefined
  this.key = data && data.key
  this.componentOptions = componentOptions
  this.componentInstance = undefined
  this.parent = undefined
  this.raw = false
  this.isStatic = false
  this.isRootInsert = true
  this.isComment = false
  this.isCloned = false
  this.isOnce = false
 }

 // DEPRECATED: alias for componentInstance for backwards compat.
 /* istanbul ignore next */
 get child (): Component | void {
  return this.componentInstance
 }
}

二、大家选择水晶色节点表明:0、壹、2标识节点序号 , 头、尾
分别代表节点的底部和节点的尾巴

每二个vnode都映射到1个实在的dom节点上。当中多少个比较重大的性子:

   相比序号从0、0(分别对应老节点序号0,新节点序号0)初阶,每1遍巡回相比三个节点数据(使用sameVNode方法判断,见上面代码示例,源码585八行),新老比较范围为
,老(0-二)新(0-二)

  1. tag 属性即那几个vnode的竹签属性
  2. data
    属性蕴涵了最终渲染成真正dom节点后,节点上的class,attribute,style以及绑定的事件
  3. children 属性是vnode的子节点
  4. text 属性是文本属性
  5. elm 属性为这几个vnode对应的真实dom节点
  6. key 属性是vnode的符号,在diff进程中得以提升diff的效用,后文有教书

(一)头头,即老0->新0 假使一致,则新老初叶节点 序号 +一 并且,跳出循环
,初步下三个循环往复相比 新老相比范围为 ,老(一-贰)新(一-2)

比如,笔者定义了二个vnode,它的数据结构是:

(二)尾尾,即老二->新贰 尽管相同,则新老甘休节点 序号 -一 同时,跳出循环
,开始下二个巡回比较 新老相比范围为 ,老(0-一)新(0-1)

  {
    tag: 'div'
    data: {
      id: 'app',
      class: 'page-box'
    },
    children: [
      {
        tag: 'p',
        text: 'this is demo'
      }
    ]
  }

(三)头尾,即老0->新二 假如壹致,则老起第1节点 序号
+一,新得了节点序号-一  同时,跳出循环
,开首下二个循环往复比较 新老相比较范围为 ,老(壹-贰)新(0-壹)

最终渲染出的实际的dom结构正是:

(四)尾头,即老二->新0 即使同样,则老停止节点 序号 -一,新起来节点序号+一  同时,跳出循环
,初步下3个循环相比 新老相比范围为 ,老(0-一)新(1-二)

  <div id="app" class="page-box">
    <p>this is demo</p>
  </div>

(5)用新节点去未遍历过的的老节点中找找

让大家再回来patch函数其中,在当oldVnode不设有的时候,那个时候是root节点起首化的长河,由此调用了createElm(vnode,
insertedVnodeQueue, parentElm,
refElm)方法去创设多少个新的节点。而当oldVnode是vnode且sameVnode(oldVnode,
vnode)1个节点的着力质量相同,那么就进去了一个节点的diff进程。

(6)老节点全体遍历完或新节点全体遍历完,则脱离大循环(对未遍历到的老节点执行删除操作,对未遍历到的新节点执行新增操作)

diff的经过首借使因此调用patchVnode方法实行的:

function sameVnode (a, b) {
  return (
    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}  
function patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) {
  ...
}


if (isDef(data) && isPatchable(vnode)) {
   // cbs保存了hooks钩子函数: 'create', 'activate', 'update', 'remove', 'destroy'
   // 取出cbs保存的update钩子函数,依次调用,更新attrs/style/class/events/directives/refs等属性
   for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
   if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
  }

 

更新真实dom节点的data属性,也正是对dom节点开始展览了预处理的操作

差不多如图:

接下来:

4858美高梅 12

  ...
  const elm = vnode.elm = oldVnode.elm
  const oldCh = oldVnode.children
  const ch = vnode.children
  // 如果vnode没有文本节点
  if (isUndef(vnode.text)) {
   // 如果oldVnode的children属性存在且vnode的属性也存在
   if (isDef(oldCh) && isDef(ch)) {
    // updateChildren,对子节点进行diff
    if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
   } else if (isDef(ch)) {
    // 如果oldVnode的text存在,那么首先清空text的内容
    if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
    // 然后将vnode的children添加进去
    addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
   } else if (isDef(oldCh)) {
    // 删除elm下的oldchildren
    removeVnodes(elm, oldCh, 0, oldCh.length - 1)
   } else if (isDef(oldVnode.text)) {
    // oldVnode有子节点,而vnode没有,那么就清空这个节点
    nodeOps.setTextContent(elm, '')
   }
  } else if (oldVnode.text !== vnode.text) {
   // 如果oldVnode和vnode文本属性不同,那么直接更新真是dom节点的文本元素
   nodeOps.setTextContent(elm, vnode.text)
  }

 问二:子组件中显著有watch value,为何this.$emit(‘input’,
88八);未有触发watch回调,反而在父组件data数据变化后触发回调?

那之中的diff进度中又分了少数种情形,oldCh为oldVnode的子节点,ch为Vnode的子节点:

难题根源(element-ui el-select
不能选中难题浅析)

  1. 先是进行文本节点的判断,若oldVnode.text !==
    vnode.text,那么就会间接举行理文件本节点的更迭;
  2. 在vnode未有公文节点的场所下,进入子节点的diff;
  3. 当oldCh和ch都设有且不雷同的图景下,调用updateChildren对子节点开始展览diff;
  4. 若oldCh不存在,ch存在,首先清空oldVnode的文书节点,同时调用addVnodes方法将ch添加到elm真实dom节点个中;
  5. 若oldCh存在,ch不设有,则删除elm真实节点下的oldCh子节点;
  6. 若oldVnode有文件节点,而vnode未有,那么就清空那些文件节点。
    <child v-model="b"></child> // b在父组件中没有初始化,即没有执行父组件的observe
    var child = Vue.extend({
      template: '<p>数据响应及render相关-案例说明</p>',
      props: {
        value: {
          required: true
        }
      },
      mounted() {
        this.$emit('input', 888);
      },
      watch: {
        value(val, oldVal) {
          console.log(val, oldVal, 'child')
        }
      }
    });

那边首要分析下updateChildren方法,它也是全体diff进度中最要紧的环节:

 涉及 知识点 observe、render、watch**

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
  // 为oldCh和newCh分别建立索引,为之后遍历的依据
  let oldStartIdx = 0
  let newStartIdx = 0
  let oldEndIdx = oldCh.length - 1
  let oldStartVnode = oldCh[0]
  let oldEndVnode = oldCh[oldEndIdx]
  let newEndIdx = newCh.length - 1
  let newStartVnode = newCh[0]
  let newEndVnode = newCh[newEndIdx]
  let oldKeyToIdx, idxInOld, elmToMove, refElm

  // 直到oldCh或者newCh被遍历完后跳出循环
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
   if (isUndef(oldStartVnode)) {
    oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
   } else if (isUndef(oldEndVnode)) {
    oldEndVnode = oldCh[--oldEndIdx]
   } else if (sameVnode(oldStartVnode, newStartVnode)) {
    patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
    oldStartVnode = oldCh[++oldStartIdx]
    newStartVnode = newCh[++newStartIdx]
   } else if (sameVnode(oldEndVnode, newEndVnode)) {
    patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
    oldEndVnode = oldCh[--oldEndIdx]
    newEndVnode = newCh[--newEndIdx]
   } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
    patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
    canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
    oldStartVnode = oldCh[++oldStartIdx]
    newEndVnode = newCh[--newEndIdx]
   } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
    patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
    // 插入到老的开始节点的前面
    canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
    oldEndVnode = oldCh[--oldEndIdx]
    newStartVnode = newCh[++newStartIdx]
   } else {
    // 如果以上条件都不满足,那么这个时候开始比较key值,首先建立key和index索引的对应关系
    if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
    idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null
    // 如果idxInOld不存在
    // 1. newStartVnode上存在这个key,但是oldKeyToIdx中不存在
    // 2. newStartVnode上并没有设置key属性
    if (isUndef(idxInOld)) { // New element
     // 创建新的dom节点
     // 插入到oldStartVnode.elm前面
     // 参见createElm方法
     createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
     newStartVnode = newCh[++newStartIdx]
    } else {
     elmToMove = oldCh[idxInOld]
     /* istanbul ignore if */
     if (process.env.NODE_ENV !== 'production' && !elmToMove) {
      warn(
       'It seems there are duplicate keys that is causing an update error. ' +
       'Make sure each v-for item has a unique key.'
      )

     // 将找到的key一致的oldVnode再和newStartVnode进行diff
     if (sameVnode(elmToMove, newStartVnode)) {
      patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
      oldCh[idxInOld] = undefined
      // 移动node节点
      canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
      newStartVnode = newCh[++newStartIdx]
     } else {
      // same key but different element. treat as new element
      // 创建新的dom节点
      createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
      newStartVnode = newCh[++newStartIdx]
     }
    }
   }
  }
  // 如果最后遍历的oldStartIdx大于oldEndIdx的话
  if (oldStartIdx > oldEndIdx) {    // 如果是老的vdom先被遍历完
   refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
   // 添加newVnode中剩余的节点到parentElm中
   addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
  } else if (newStartIdx > newEndIdx) { // 如果是新的vdom先被遍历完,则删除oldVnode里面所有的节点
   // 删除剩余的节点
   removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
  }
}

题材原理:

在始发遍历diff前,首先给oldCh和newCh分别分配三个startIndex和endIndex来作为遍历的目录,当oldCh只怕newCh遍历完后(遍历完的规范正是oldCh可能newCh的startIndex
>=
endIndex),就截至oldCh和newCh的diff进程。接下来通超过实际例来看下整个diff的长河(节点属性中不带key的景观):

1、vue实例在转移时,会经历比较遥远的开端化进程如:起始化事件、渲染器、注入器、数据代理\绑架等等

先是从第3个节点起先比较,不管是oldCh依然newCh的苗头或然终止节点都不设有sameVnode,同时节点属性中是不带key标记的,由此首先轮的diff完后,newCh的startVnode被添加到oldStartVnode的日前,同时newStartIndex前移一个人;

二、v-model指令为被默许构造 :value=”b” @input=”b = xxx”
(此处b为地点代码中v-model=”b”),执行this.$emit(‘input’, 88八),会更新b的值

4858美高梅 13

三、每一回data数据变动的时候,会收集watcher进入1个队列,队列中募集了全部此时刻周期中有有关变更形成queue沃特cher,在下三个年华周期中进行数量响应更新 flushSchedulerQueue,并推行render 、patch

其次轮的diff中,满意sameVnode(oldStartVnode,
newStartVnode),因而对这个vnode举行diff,最终将patch打到oldStartVnode上,同时oldStartVnode和newStartIndex都向前移动1位

4、在render中会举行新老vnode
patch,在更新进程中会执行node的prepatch(源码5938行)操作,个中会进行updateChildComponent
-> validateProp 从而立异了
value的值,从而进入了watch回调

4858美高梅 14

大约如图:

其三轮车的diff中,满足sameVnode(oldEndVnode,
newStartVnode),那么首先对oldEndVnode和newStartVnode举行diff,并对oldEndVnode实行patch,并成功oldEndVnode移位的操作,最后newStartIndex前移一个人,oldStartVnode后移1个人;

4858美高梅 15

4858美高梅 16

    

第陆轮的diff中,进度同步骤3;

 by:海豚湾-丰

4858美高梅 17

   

第五轮的diff中,同进度1;

4858美高梅 18

遍历的经过甘休后,newStartIdx >
newEndIdx,表达此时oldCh存在多余的节点,那么最后就供给将这么些多余的节点删除。

4858美高梅 19

在vnode不带key的景色下,每1轮的diff进度当中都以开头和得了节点实行相比,直到oldCh或许newCh被遍历完。而当为vnode引进key属性后,在每1轮的diff进度中,当开端和了结节点都尚未找到sameVnode时,首先对oldCh中进行key值与索引的映照:

if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null

createKeyToOldIdx方法,用以将oldCh中的key属性作为键,而相应的节点的目录作为值。然后再判断在newStartVnode的习性中是或不是有key,且是或不是在oldKeyToIndx中找到相应的节点。

要是不设有那么些key,那么就将这些newStartVnode作为新的节点创立且插入到原始的root的子节点中:

if (isUndef(idxInOld)) { // New element
  // 创建新的dom节点
  // 插入到oldStartVnode.elm前面
  // 参见createElm方法
  createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
     newStartVnode = newCh[++newStartIdx]
    } 

若是存在那么些key,那么就取出oldCh中的存在这些key的vnode,然后再开展diff的经过:

elmToMove = oldCh[idxInOld]
     /* istanbul ignore if */
     if (process.env.NODE_ENV !== 'production' && !elmToMove) {

     // 将找到的key一致的oldVnode再和newStartVnode进行diff
     if (sameVnode(elmToMove, newStartVnode)) {
      patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
      // 清空这个节点
      oldCh[idxInOld] = undefined
      // 移动node节点
      canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
      newStartVnode = newCh[++newStartIdx]
     } else {
      // same key but different element. treat as new element
      // 创建新的dom节点
      createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
      newStartVnode = newCh[++newStartIdx]
     }

经过以上分析,给vdom上添加key属性后,遍历diff的历程中,当初阶点,
停止点的搜寻及diff出现依旧无力回天同盟的景况下时,就会用key来作为唯1标识,来进行diff,那样就能够抓牢diff作用。

饱含Key属性的vnode的diff进度可知下图:

注意在首先轮的diff过后oldCh上的B节点被剔除了,可是newCh上的B节点上elm属性保持对oldCh上B节点的elm引用。

4858美高梅 20

4858美高梅 21

4858美高梅 22

4858美高梅 23

4858美高梅 24

上述正是本文的全体内容,希望对大家的就学抱有援助,也盼望大家多多支持脚本之家。

你恐怕感兴趣的篇章:

  • 在vue中拿走dom成分内容的秘诀
  • vue的Virtual
    Dom实现snabbdom解密
  • vue动态生成dom并且自动绑定事件
  • 使用vue.js插入dom节点的不贰诀窍
  • Vue获取DOM成分样式和样式更改示例
  • vue指令以及dom操作详解
  • 详解在Vue中经过自定义指令获取dom成分
  • Vue.js 二.0偷窥之Virtual
    DOM到底是怎么着?
  • 商讨Vue.js
    二.0新增的虚拟DOM
  • Vue AST源码解析第三篇

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图
Copyright @ 2010-2019 美高梅手机版4858 版权所有