资讯专栏INFORMATION COLUMN

Vue 2.0源码学习

Joyven / 2693人阅读

摘要:今年的月日,的版本正式发布了,其中核心代码都进行了重写,于是就专门花时间,对的源码进行了学习。本篇文章就是源码学习的总结。实现了并且将静态子树进行了提取,减少界面重绘时的对比。的最新源码可以去获得。

Vue2.0介绍

从去年9月份了解到Vue后,就被他简洁的API所吸引。1.0版本正式发布后,就在业务中开始使用,将原先jQuery的功能逐步的进行迁移。
今年的10月1日,Vue的2.0版本正式发布了,其中核心代码都进行了重写,于是就专门花时间,对Vue 2.0的源码进行了学习。本篇文章就是2.0源码学习的总结。

先对Vue 2.0的新特性做一个简单的介绍:

大小 & 性能。Vue 2.0的线上包gzip后只有12Kb,而1.0需要22Kb,react需要44Kb。而且,Vue 2.0的性能在react等几个框架中,性能是最快的。

VDOM。实现了Virtual DOM, 并且将静态子树进行了提取,减少界面重绘时的对比。与1.0对比性能有明显提升。

template & JSX。众所周知,Vue 1.0使用的是template来实现模板,而React使用了JSX实现模板。关于template和JSX的争论也很多,很多人不使用React就是因为没有支持template写法。Vue 2.0对template和JSX写法都做了支持。使用时,可以根据具体业务细节进行选择,可以很好的发挥两者的优势。就这一点,Vue已经超过React了。

Server Render。2.0还对了Server Render做了支持。这一点并没有在业务中使用,不做评价。

Vue的最新源码可以去 https://github.com/vuejs/vue 获得。本文讲的是 2.0.3版本,2.0.3可以去 https://github.com/vuejs/vue/... 这里获得。

下面开始进入正题。首先从生命周期开始。

生命周期

上图就是官方给出的Vue 2.0的生命周期图,其中包含了Vue对象生命周期过程中的几个核心步骤。了解了这几个过程,可以很好的帮助我们理解Vue的创建与销毁过程。
从图中我们可以看出,生命周期主要分为4个过程:

createnew Vue时,会先进行create,创建出Vue对象。

mount。根据el, template, render方法等属性,会生成DOM,并添加到对应位置。

update。当数据发生变化后,会重新渲染DOM,并进行替换。

destory。销毁时运行。

那么这4个过程在源码中是怎么实现的呢?我们从new Vue开始。

new Vue

为了更好的理解new的过程,我整理了一个序列图:

new Vue的过程主要涉及到三个对象:vm、compiler、watcher。其中,vm表示Vue的具体对象;compiler负责将template解析为AST render方法;watcher用于观察数据变化,以实现数据变化后进行re-render。

下面来分析下具体的过程和代码:
首先,运行new Vue()的时候,会进入代码src/core/instance/index.js的Vue构造方法中,并执行this._init()方法。在_init中,会对各个功能进行初始化,并执行beforeCreatecreated两个生命周期方法。核心代码如下:

initLifecycle(vm)
initEvents(vm)
callHook(vm, "beforeCreate")
initState(vm)
callHook(vm, "created")
initRender(vm)

这个过程有一点需要注意:
beforeCreate和created之间只有initState,和官方给出的生命周期图并不完全一样。这里的initState是用于初始化data,props等的监听的。

_init的最后,会运行initRender方法。在该方法中,会运行vm.$mount方法,代码如下:

if (vm.$options.el) {
  vm.$mount(vm.$options.el)
}

这里的vm.$mount可以在业务代码中调用,这样,new 过程和 mount过程就可以根据业务情况进行分离。

这里的$mountsrc/entries/web-runtime-with-compiler.js中,主要逻辑是根据el, template, render三个属性来获得AST render方法。代码如下:

if (!options.render) {   // 如果有render方法,直接运行mount
  let template = options.template
  if (template) {  // 如果有template, 获取template参数对于的HTML作为模板
    if (typeof template === "string") {
      if (template.charAt(0) === "#") {
        template = idToTemplate(template)
      }
    } else if (template.nodeType) {
      template = template.innerHTML
    } else {
      if (process.env.NODE_ENV !== "production") {
        warn("invalid template option:" + template, this)
      }
      return this
    }
  } else if (el) {  // 如果没有template, 且存在el,则获取el的outerHTML作为模板
    template = getOuterHTML(el)
  }
  if (template) { // 如果获取到了模板,则将模板转化为render方法
    const { render, staticRenderFns } = compileToFunctions(template, {
      warn,
      shouldDecodeNewlines,
      delimiters: options.delimiters
    }, this)
    options.render = render
    options.staticRenderFns = staticRenderFns
  }
}
return mount.call(this, el, hydrating)

这个过程有三点需要注意:
compile时,将最大静态子树提取出来作为多带带的AST渲染方法,以提升后面vNode对比时的性能。所以,当存在多个连续的静态标签时,可以在外边添加一个静态父节点,这样,staticRenderFns数目可以减少,从而提升性能。
Vue 2.0中的模板有三种引用写法:el, template, render(JSX)。其中的优先级是render > template > el。
el, template两种写法,最后都会通过compiler转化为render(JSX)来运行,也就是说,直接写成render(JSX)是性能最优的。当然,如果使用了构建工具,最终生成的包就是使用的render(JSX)。这样子,在源码上就可以不用过多考虑这一块的性能了,直接用可维护性最好的方式就行。

将模板转化为render,用到了compileToFunctions方法,该方法最后会通过src/compiler/index.js文件中的compile方法,将模板转化为AST语法结构的render方法,并对静态子树进行分离。

完成render方法的生成后,会进入_mount(src/core/instance.lifecycle.js)中进行DOM更新。该方法的核心逻辑如下:

vm._watcher = new Watcher(vm, () => {
  vm._update(vm._render(), hydrating)
}, noop)

首先会new一个watcher对象,在watcher对象创建后,会运行传入的方法vm._update(vm._render(), hydrating)(watcher的逻辑在下面的watcher小节中细讲)。其中的vm._render()主要作用就是运行前面compiler生成的render方法,并返回一个vNode对象。这里的vNode就是一个虚拟的DOM节点。

拿到vNode后,传入vm._update()方法,进行DOM更新。

VDOM

上面已经讲完了new Vue过程中的主要步骤,其中涉及到template如何转化为DOM的过程,这里多带带拿出来讲下。先上序列图:

从图中可以看出,从template到DOM,有三个过程:

template -> AST render (compiler解析template)

AST render -> vNode (render方法运行)

vNode -> DOM (vdom.patch)

首先是template在compiler中解析为AST render方法的过程。上一节中有说到,initRender后,会调用到src/entries/web-runtime-with-compiler.js中的Vue.prototype.$mount方法。在$mount中,会获取template,然后调用src/platforms/web/compiler/index.jscompileToFunctions方法。在该方法中,会运行compile将template解析为多个render方法,也就是AST render。这里的compile在文件src/compiler/index.js中,代码如下:

const ast = parse(template.trim(), options)   // 解析template为AST
optimize(ast, options)  // 提取static tree
const code = generate(ast, options)  // 生成render 方法
return {
  ast,
  render: code.render,
  staticRenderFns: code.staticRenderFns
}

可以看出,compile方法就是将template以AST的方式进行解析,并转化为render方法进行返回。

再看第二个过程:AST render -> vNode。这个过程很简单,就是将AST render方法进行运行,获得返回的vNode对象。

最后一步,vNode -> DOM。该过程中,存在vNode的对比以及DOM的添加修改操作。
在上一节中,有讲到vm._update()方法中对DOM进行更新。_update的主要代码如下:

// src/core/instance/lifecycle.js
if (!prevVnode) {
  // Vue.prototype.__patch__ is injected in entry points
  // based on the rendering backend used.
  vm.$el = vm.__patch__(vm.$el, vnode, hydrating)  // 首次添加
} else {
  vm.$el = vm.__patch__(prevVnode, vnode)  // 数据变化后触发的DOM更新
}

可以看出,无论是首次添加还是后期的update,都是通过__patch__来更新的。这里的__patch__核心步骤是在src/core/vdom/patch.js中的patch方法进行实现,源码如下:

function patch (oldVnode, vnode, hydrating, removeOnly) {
    if (!oldVnode) {
      ...
    } else {
      ...
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)  // diff并更新DOM。
      } else {
        elm = oldVnode.elm
        parent = nodeOps.parentNode(elm)
        ...
        if (parent !== null) {
          nodeOps.insertBefore(parent, vnode.elm, nodeOps.nextSibling(elm))  // 添加element到DOM。
          removeVnodes(parent, [oldVnode], 0, 0)
        }
        ...
      }
    }
    ...
  }

首次添加很简单,就是通过insertBefore将转化好的element添加到DOM中。如果是update,则会调动patchVnode()。最后来看下patchVnode的代码:

function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
  ...
  const elm = vnode.elm = oldVnode.elm
  const oldCh = oldVnode.children
  const ch = vnode.children
  ...
  if (isUndef(vnode.text)) {
    if (isDef(oldCh) && isDef(ch)) {  // 当都存在时,更新Children
      if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    } else if (isDef(ch)) {  // 只存在新节点时,即添加节点
      if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, "")
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    } else if (isDef(oldCh)) {  // 只存在老节点时,即删除节点
      removeVnodes(elm, oldCh, 0, oldCh.length - 1)
    } else if (isDef(oldVnode.text)) {  // 删除了textContent
      nodeOps.setTextContent(elm, "")
    }
  } else if (oldVnode.text !== vnode.text) { // 修改了textContent
    nodeOps.setTextContent(elm, vnode.text)
  }
}

其中有调用了updateChildren来更新子节点,代码如下:

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
  ...
  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 {
      ...
    }
  }
  ...
}

可以看到updateChildren中,又通过patchVnode来更新当前节点。梳理一下,patch通过patchVnode来更新根节点,然后通过updateChildren来更新子节点,具体子节点,又通过patchVnode来更新,通过一个类似于递归的方式逐个节点的完成对比和更新。

Vue 2.0中对如何去实现VDOM的思路是否清晰,通过4层结构,很好的实现了可维护性,也为实现server render, weex等功能提供了可能。拿server render举例,只需要将最后的vNode -> DOM 改成 vNode -> String 或者 vNode -> Stream, 就可以实现server render。剩下的compiler和Vue的核心逻辑都不需要改。

Watcher

我们都知道MVVM框架的特征就是当数据发生变化后,会自动更新对应的DOM节点。使用MVVM之后,业务代码中就可以完全不写DOM操作代码,不仅可以将业务代码聚焦在业务逻辑上,还可以提高业务代码的可维护性和可测试性。那么Vue 2.0中是怎么实现对数据变化的监听的呢?照例,先看序列图:

可以看出,整个Watcher的过程可以分为三个过程。

对state设置setter/getter

对vm设置好Watcher,添加好state 触发 setter时的执行方法

state变化触发执行

前面有说过,在生命周期函数beforeCreatecreated直接,会运行方法initState()。在initState中,会对Props, Data, Computed等属性添加Setter/Getter。拿Data举例,设置setter/getter的代码如下:

function initData (vm: Component) {
  let data = vm.$options.data
  ...
  // proxy data on instance
  const keys = Object.keys(data)
  let i = keys.length
  while (i--) {
    ...
    proxy(vm, keys[i])   // 设置vm._data为代理
  }
  // observe data
  observe(data)
}

通过调用observe方法,会对data添加好观察者,核心代码为:

Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter () {
    const value = getter ? getter.call(obj) : val
    if (Dep.target) {
      dep.depend()  // 处理好依赖watcher
      ...
    }
    return value
  },
  set: function reactiveSetter (newVal) {
    ...
    childOb = observe(newVal)  // 对新数据重新observe
    dep.notify()  // 通知到dep进行数据更新
  }
})

这个时候,对data的监听已经完成。可以看到,当data发生变化的时候,会运行dep.notify()。在notify方法中,会去运行watcher的update方法,内容如下:

update () {
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}
run () {
  if (this.active) {
    const value = this.get()
  }
  ...
}

update方法中,queueWatcher方法的目的是通过nextTicker来执行run方法,属于支线逻辑,就不分析了,这里直接看run的实现。run方法其实很简单,就是调用get方法,而get方法会通过执行this.getter()来更新DOM。

那么this.getter是什么呢?本文最开始分析new Vue过程时,有讲到运行_mount方法时,会运行如下代码:

vm._watcher = new Watcher(vm, () => {
  vm._update(vm._render(), hydrating)
}, noop)

那么this.getter就是这里Watcher方法的第二个参数。来看下new Watcher的代码:

export default class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: Object = {}
  ) {
    ...
    if (typeof expOrFn === "function") {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    ...
    this.value = this.lazy
      ? undefined
      : this.get()
  }
}

可以看出,在new Vue过程中,Watcher会在构造完成后主动调用this.get()来触发this.getter()方法的运行,以达到更新DOM节点。

总结一下这个过程:首先_init时,会对Data设置好setter方法,setter方法中会调用dep.notify(),以便数据变化时通知DOM进行更新。然后new Watcher时,会将更新DOM的方法进行设置,也就是Watcher.getter方法。最后,当Data发生变化的时候,dep.notify()运行,运行到watcher.getter()时,就会去运行render和update逻辑,最终达到DOM更新的目的。

总结与收获

刚开始觉得看源码,是因为希望能了解下Vue 2.0的实现,看看能不能得到一些从文档中无法知道的细节,用于提升运行效率。把主要流程理清楚后,的确了解到一些,这里做个整理:

el属性传入的如果不是element,最后会通过document.querySelector来获取的,这个接口性能较差,所以,el传入一个element性能会更好。

$mount方法中对htmlbody标签做了过滤,这两个不能用来作为渲染的根节点。

每一个组件都会从_init开始重新运行,所以,当存在一个长列表时,将子节点作为一个组件,性能会较差。

*.vue文件会在构建时转化为render方法,而render方法的性能比指定template更好。所以,源码使用*.vue的方式,性能更好。

如果需要自定义delimiters,每一个组件都需要多带带指定。

如果是*.vue文件,指定delimiters是失效的,因为vue-loader*.vue文件进行解析时,并没有将delimiters传递到compiler.compile()中。(这一点不确定是bug还是故意这样设计的)。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/80906.html

相关文章

  • vue@2.0源码学习---目录结构分析与准备工作

    摘要:源码版本为原文地址和有必要了解这两个概念的区别。点开目录下的,发现确实是导出了一个构造函数。再回过头看,它给构造函数扩展了一些方法具体的逻辑后文看。 前言 网上vue的源码分析也蛮多的,不过很多都是1.0版本的并且大多都是在讲数据的observe,索性自己看看源码,虽然很难但是希望能学到点东西。 源码版本为2.0.0 原文地址 runtime和runtime-with-compiler...

    tinysun1234 评论0 收藏0
  • vue@2.0源码学习---组件究竟是什么

    摘要:定义一个组件如下打印如下再回过头看,可以发现他做的工作就是扩展一个构造函数,并将这个构造函数添加到现在我们已经可以回答最开始的问题的组件是什么的组件其实就是扩展的构造函数,并且在适当的时候实例化为实例。 vue@2.0源码学习---组件究竟是什么 本篇文章从最简单的情况入手,不考虑prop和组件间通信。 Vue.component vue文档告诉我们可以使用Vue.component(...

    PiscesYE 评论0 收藏0
  • Vue 2.0】核心源码解读 -- 不定期更新

    摘要:观察员由模板解析指令创建的观察员负责模板中的更新视图操作。观察员种类目前了解情况来看主要分三类视图指令的计算属性的用户自定义的 介绍 关于 Vue.js 的原理一直以来都是一个话题。经过几天的源码学习和资料介绍,我将一些个人理解的经验给写下来,希望能够与大家共勉。 附上GITHUB源码地址, 如果有任何不解 可以在 文章下面提出或者写下issue, 方便大家回答和学习, 有兴趣可以St...

    sunsmell 评论0 收藏0
  • Vue 笔记三:Vue2.0与1.0的区别

    摘要:升级的区别与的断层式升级不同,延续了自己的风格。在命名方式和上有一些区别,掌握它们是你升级整个项目的关键。以下内容都是来源于个人项目的一些经验之谈,并非系统性的阐述。总目录前端经验收集器转载自个人建了前端学习群,旨在一起学习前端。 升级的区别 与angular的断层式升级不同,vue延续了自己的风格。在命名方式和API上有一些区别,掌握它们是你升级整个项目的关键。以下内容都是来源于个人...

    Big_fat_cat 评论0 收藏0
  • 前端窝 - 收藏集 - 掘金

    摘要:毫无疑问,设计模式于己于他人于系统都是多赢的设计模式使代码编写真正工程化设计模小书前端掘金这是一本关于的小书。 JavaScript 常见设计模式解析 - 掘金设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。毫无疑问,设计模式于己于他人于系统都是多赢的;设计...

    李文鹏 评论0 收藏0

发表评论

0条评论

Joyven

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<