资讯专栏INFORMATION COLUMN

Vue2 源码分析

alin / 1658人阅读

摘要:应用启动一般是通过,所以,先从该构造函数着手。构造函数文件该文件只是构造函数,原型对象的声明分散在当前目录的多个文件中构造函数接收参数,然后调用。

源码版本: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 的了解,见【链接】中之前的文章。

水平有限,错漏难免,欢迎指正。

感谢阅读!

链接

Vue 双向数据绑定原理分析 - luobotang

一起理解 Virtual DOM - luobotang

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

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

相关文章

  • vue2.0源码分析之理解响应式架构

    摘要:分享前啰嗦我之前介绍过如何实现和。我们采用用最精简的代码,还原响应式架构实现以前写的那篇源码分析之如何实现和可以作为本次分享的参考。到现在为止,我们再看那张图是不是就清楚很多了总结我非常喜欢,以上代码为了好展示,都采用最简单的方式呈现。 分享前啰嗦 我之前介绍过vue1.0如何实现observer和watcher。本想继续写下去,可是vue2.0横空出世..所以 直接看vue2.0吧...

    chenatu 评论0 收藏0
  • vue2源码框架和流程分析

    摘要:流程图盗用一下官网关于生命周期的图,对照之前的内容梳理一下对照上面的分析基本上可以找到各个钩子函数的位置,下面那个销毁的我就没用做分析了。。。 vue整体框架和主要流程分析 之前对看过比较多关于vue源码的文章,但是对于整体框架和流程还是有些模糊,最后用chrome debug对vue的源码进行查看整理出这篇文章。。。。 本文对vue的整体框架和整体流程进行简要的分析,不对某些具体的细...

    tain335 评论0 收藏0
  • Vue2 transition源码分析

    摘要:至此算是找到了源码位置。至此进入过渡的部分完毕。在动画结束后,调用了由组件生命周期传入的方法,把这个元素的副本移出了文档流。这篇并没有去分析相关的内容,推荐一篇讲非常不错的文章,对构造函数如何来的感兴趣的同学可以看这里 Vue transition源码分析 本来打算自己造一个transition的轮子,所以决定先看看源码,理清思路。Vue的transition组件提供了一系列钩子函数,...

    Genng 评论0 收藏0

发表评论

0条评论

alin

|高级讲师

TA的文章

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