Vue2 源码剖析

源码版本:v2.1.10

剖析目的

经由历程浏览源码,对 Vue2 的基础运行机制有所相识,主假如:

  • Vue2 中数据绑定的完成体式格局

  • Vue2 中对 Virtual DOM 机制的运用体式格局

源码初见

项目构建设置文件为 build/config.js,定位 vue.js 对应的进口文件为 src/entries/web-runtime-with-compiler.js,基于 rollup 举行模块打包。

代码中运用 flow 举行接口类型标记和搜检,在打包历程当中移除这些标记。为了浏览代码轻易,在 VS Code 中安装了插件 Flow Language Support,然后封闭事情区 JS 代码搜检,如许界面就清新很多了。

Vue 运用启动平常是经由历程 new Vue({...}),所以,先从该组织函数动手。

注:本文只关注 Vue 在浏览器端的运用,不触及服务器端代码。

Vue 组织函数

文件:src/core/instance/index.js

该文件只是组织函数,Vue 原型对象的声明疏散在当前目次的多个文件中:

  • init.js:._init()

  • state.js:.$data .$set() .$delete() .$watch()

  • render.js:._render()

  • events.js:.$on() .$once() .$off() .$emit()

  • lifecycle.js:._mount() ._update() .$forceUpdate() .$destroy()

组织函数吸收参数 options ,然后挪用 this._init(options)

._init() 中举行初始化,个中会顺次挪用 lifecycle、events、render、state 模块中的初始化函数。

Vue2 中应当是为了代码更容易治理,Vue 类的定义疏散到了上面的多个文件中。

个中,关于 Vue.prototype 对象的定义,经由历程 mixin 的体式格局在进口文件 core/index.js 中顺次挪用。关于实例对象(代码中一般称为 vm)则经由历程 init 函数在 vm._init() 中顺次挪用。

Vue 大众接口

文件:src/core/index.js

这里挪用了 initGlobalAPI() 来初始化 Vue 的大众接口,包含:

  • Vue.util

  • Vue.set

  • Vue.delete

  • Vue.nextTick

  • Vue.options

  • Vue.use

  • Vue.mixin

  • Vue.extend

  • asset相干接口:设置在 src/core/config.js

Vue 启动历程

挪用 new Vue({...}) 后,在内部的 ._init() 的末了,是挪用 .$mount() 要领来“启动”。

web-runtime-with-compiler.jsweb-runtime.js 中,定义了 Vue.prototype.$mount()。不过两个文件中的 $mount() 终究挪用的是 ._mount() 内部要领,定义在文件 src/core/instance/lifecycle.js 中。

Vue.prototype._mount(el, hydrating)

简化逻辑后的伪代码:

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

接下来看 Watcher

Watcher

文件:src/core/observer/watcher.js

先看组织函数的简化逻辑:

// 参数:vm, expOrFn, cb, options
this.vm = vm
vm._watchers.push(this)
// 剖析 options,略....
// 属性初始化,略....
this.getter = expOrFn // if `function`
this.value = this.lazy ? undefined : this.get()

因为缺省的 lazy 属性值为 false,接着看 .get() 的逻辑:

pushTarget(this) // !
value = this.getter.call(this.vm, this.vm)
popTarget()
this.cleanupDeps()
return value

先看这里对 getter 的挪用,返回到 ._mount() 中,能够看到,是挪用了 vm._update(vm._render(), hydrating),触及两个要领:

  • vm._render():返回假造节点(VNode)

  • vm._update()

来看 _update() 的逻辑,这里应当是举行 Virtual DOM 的更新:

// 参数:vnode, hydrating
vm = this
prevEl = vm.$el
prevVnode = vm._vnode
prevActiveInstance = activeInstance
activeInstance = vm
vm._vnode = vnode
if (!prevVnode) {
  // 首次加载
  vm.$el = vm.__patch__(vm.$el, vnode, ...)
} else {
  // 更新
  vm.$el = vm.__patch__(prevVnode, vnode)
}
activeInstance = prevActiveInstance
// 后续属性设置,略....

参考 Virtual DOM 的平常逻辑,这里是差不多的处置惩罚历程,不再赘述。

综上,这里的 watcher 重要作用应当是在数据发作变动时,触发从新衬着和更新视图的处置惩罚:vm._update(vm._render())

接下来,我们看下 watcher 是怎样发挥作用的,参考 Vue 1.0 的履历,下面应当是关于依靠网络、数据绑定方面的细节了,而这一部分,和 Vue 1.0 差异不大。

数据绑定

watcher.get() 中挪用的 pushTarget()popTarget() 来自文件:src/core/observer/dep.js

pushTarget()popTarget() 两个要领,用于处置惩罚 Dep.target,明显 Dep.targetwather.getter 的挪用历程当中会用到,挪用时会触及到依靠网络,从而建立起数据绑定的关联。

Dep 类的 .dep() 要领顶用到了 Dep.target,挪用体式格局为:

Dep.target.addDep(this)

能够想见,在运用数据举行衬着的历程当中,会对数据属性举行“读”操纵,从而触发 dep.depend(),进而网络到这个依靠关联。下面来找一下如许的挪用的位置。

state.js 中找到一处,makeComputedGetter() 函数中经由历程 watcher.depend() 间接挪用了 dep.depend()。不过 computedGetter 应当不是最重要的处所,依据 Vue 1.0 的履历,照样要找对数据举行“数据挟制”的处所,应当是defineReactive()

defineReactive() 定义在文件 src/core/observer/index.js

// 参数:obj, key, val, customSetter?
dep = new Dep()
childOb = observe(val)
Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function () {
    // 略,挪用了 dep.depend()
  },
  set: function () {
    // 略,挪用 dep.notify()
  }
})

连系 Vue 1.0 履历,这里应当就是数据挟制的症结了。数据原有的属性被从新定义,属性的 get() 被挪用时,会经由历程 dep.depend() 网络依靠关联,纪录到 vm 中;而在 set() 被挪用时,则会推断属性值是不是发作变动,假如发作变动,则经由历程 dep.notify() 来关照 vm,从而触发 vm 的更新操纵,完成 UI 与数据的同步,这也就是数据绑定后的效果了。

回过头来看 state.js,是在 initProps() 中挪用了 defineReactive()。而 initProps()initState() 中挪用,后者则是在 Vue.prototype._init() 中被挪用。

不过最经常使用的实际上是在 initData() 中,对初始传入的 data 举行挟制,不过内里的历程轻微绕一些,是将这里的 data 赋值到 vm._data 而且代办到了 vm 上,进一步的处置惩罚还触及 observe()Observer 类。这里不展开了。

综上,数据绑定的完成历程为:

  • 初始化:new Vue() -> vm._init()

  • 数据挟制:initState(vm) -> initProps(), initData() -> dep.depend()

  • 依靠网络:vm.$mount() -> vm._mount() -> new Watcher() -> vm._render()

衬着

起首来看 initRender(),这里在 vm 上初始化了两个与建立假造元素相干的要领:

  • vm._c()

  • vm.$createElement()

其内部完成都是挪用 createElement(),来自文件:src/core/vdom/create-element.js

而在 renderMixin() 中初始化了 Vue.prototype._render() 要领,个中建立 vnode 的逻辑为:

render = vm.$options.render
try {
  vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
  // ...
}

这里传入 render() 是一个会返回 vnode 的函数。

接下来看 vm._update() 的逻辑,这部分在前面有引见,首次衬着时是经由历程挪用 vm.__patch__() 来完成。那末 vm.__patch__() 是在那里完成的呢?在 _update() 代码中有句解释,提到:

    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.

在文件 web-runtime.js 中,找到了:

Vue.prototype.__patch__ = inBrowser ? patch : noop

明显示在浏览器环境下运用 patch(),来自:src/platforms/web/runtime/patch.js,其完成是经由历程 createPatchFunction(),来自文件 src/core/vdom/patch

OK,以上线索都指向了 vdom 相干的模块,也就是说,明显是 vdom 也就是 Virtual DOM 介入了衬着和更新。

不过另有个题目没有解决,那就是原始的字符串模块,是怎样转成用于 Virtual DOM 建立的函数挪用的呢?这里会有一个剖析的历程。

回到进口文件 web-runtime-with-compiler.js,在 Vue.prototype.$mount() 中,有一个症结的挪用:compileToFunctions(template, ...)template 变量值为传入的参数剖析获得的模板内容。

模板剖析

文件:src/platforms/web/compiler/index.js

函数 compileToFunctions() 的基础逻辑:

// 参数:template, options?, vm?
res = {}
compiled = compile(template, options)
res.render = makeFunction(compiled.render)
// 拷贝数组元素:
// res.staticRenderFns <= compiled.staticRenderFns
return res

这里对模板举行了编译(compile()),终究返回了依据编译效果获得的 render()、staticRenderFns。再看 web-runtime-with-compiler.jsVue.prototype.$mount() 的逻辑,则是将这里获得的效果写入了 vm.$options 中,也就是说,背面 vm._render() 中会运用这里的 render()

再来看 compile() 函数,这里是完成模板剖析的中心,来做文件 src/compiler/index.js,基础逻辑为:

// 参数:template, options
ast = parse(template.trim(), options)
optimize(ast, options)
code = generate(ast, options)
return {
  ast,
  render: code.render,
  staticRenderFns: code.staticRenderFns
}

逻辑很清楚,起首从模板举行剖析获得笼统语法树(ast),举行优化,末了天生效果代码。全部历程当中肯定会触及到 Vue 的语法,包含指令、组件嵌套等等,不仅仅是获得构建 Virtual DOM 的代码。

须要注重的是,编译获得 render 实际上是代码文本,经由历程 new Function(code) 的体式格局转为函数。

总结

Vue2 比拟 Vue1 一个重要的区分在于引入了 Virtual DOM,但其 MVVM 的特征还在,也就是说仍有一套数据绑定的机制。

另外,Virtual DOM 的存在,使得原有的视图模板须要转变为函数挪用的形式,从而在每次有更新时能够从新挪用获得新的 vnode,从而运用 Virtual DOM 的更新机制。为此,Vue2 完成了编译器(compiler),这也意味着 Vue2 的模板能够是纯文本,而不必是 DOM 元素。

Vue2 基础运行机制总结为:

  • 文本模板,编译获得天生 vnode 的函数(render),该历程当中会辨认并纪录 Vue 的指令和其他语法

  • new Vue() 获得 vm 对象,个中传入的数据会举行数据挟制处置惩罚,从而能够网络依靠,完成数据绑定

  • 衬着历程是将一切数据交由衬着函数(render)举行挪用获得 vnode,应当 Virtual DOM 的机制完成初始衬着和更新

写在末了

对 Vue2 的源码剖析,是基于我之前对 Vue1 的剖析和对 Virtual DOM 的相识,见【链接】中之前的文章。

程度有限,讹夺不免,迎接斧正。

谢谢浏览!

链接

    原文作者:luobotang
    原文地址: https://segmentfault.com/a/1190000008488543
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞