前言
最近在看 vue.js 2.x 的源码,后面会陆续的写一些笔记和大家分享和学习。
前置条件
Flow
Flow
是 facebook 出品的一个静态类型检查工具,它的语法和 Typescript
类似。vue 2.x 用了 Flow,所以没接触过的同学可能需要先了解一下。官网地址: flow.org/ 。
Virtual Dom
在学习 vue.js 之前,我们需要先了解一下 Virtual Dom —— 虚拟DOM。
目前,社区里有两个 virtual dom 库,一个是 virtual-dom , 另一个是 snabbdom 。前者已经有5年没更新了,后者还在更新,特别指出,vue 2.x 引入的 virtual dom 和 diff 算法就是基于 snabbdom 改造的。所以,建议没了解过 snabbdom
可以看看相关的文章。
- 关于Virtual DOM理解和Snabbdom源码浅析
- snabbdom 源码阅读分析
响应式(双向绑定)
关于 vue 的响应式原理,我们在看到相关代码时,再具体分析。我们现在只需要知道它是通过Object.defineProperty()
定义存取器 getter
和 setter
来实现的。
准备工作
从github上获取源码
项目地址:github.com/vuejs/vue 。fork 一份到自己仓库,克隆到本地,从 dev 创建一个 study 的学习分支,方便写注释。
源码结构
我们先看一下vue源码的目录结构,vue 源码包含以下几个目录:
src
├── compiler // 编译器,将 template 编译成 render 函数
├── core // Vue构造函数,一些静态方法,vdom和响应式原理
├── platforms // web/weex
├── server // 服务端渲染
├── sfc // *.vue 单文件组件的编译方法
└── shared // 公用的工具函数
后续我们主要需要研究的是 前三个 目录里的代码,即编译器、核心代码和 web 平台相关的代码。
如何调试
vue 2.x 是使用 rollup
编译的,添加 --sourcemap
编译选项,可以生成 sourcemap
{
"scripts": {
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"
}
}
我们可以新建一个示例,打开 chrome 开发者工具中的 source 标签,设置断点来调试。下图的 src 就是开启了sourcemap 后出现的
简单示例:
<div id="app>{{count}}</div>
<script scr="xxx/vue.js"></script> // 为了方便看源码,引用没有压缩混淆过的 vue
<script>
var vm = new Vue({
data: {
count: 1
}
})
vm.$mount("#app")
</script>
入口文件
我们直接从完整版的 vue 的入口文件开始看,即 src/platform/web/entry-runtime-with-compiler.js
。从命名上可以看出来,这是带有模版编译器的运行时版本。
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el) // document.querySelector 获取 dom 元素
const options = this.$options
// 如果没有 render 函数,则使用 template
if (!options.render) {
let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
// 如果 template 是选择器符号,则获取对应 dom 的 innerHTML。
// <script type="text/x-template>...</script>">
template = idToTemplate(template)
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
return this
}
} else if (el) {
// 如果没有 template,则把 el 的 outerHTML 作为 template
template = getOuterHTML(el)
}
if (template) {
// 将 template 转换成渲染函数
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
}
}
// 调用原先定义的 mount 方法挂载
return mount.call(this, el, hydrating)
}
带有编译器的版本,扩展了 mount
方法。如果用户没有定义 render 函数,会尝试寻找 template
模版,如果 template 是选择器描述符,则获取内部的 html ,最终都是编译成 render
函数来初始化。
mount 挂载
下面,我们转到 src/platform/web/runtime/index.js
文件中,看不带编译器的 $mount
是如何运行的。
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
获取需要挂载的 dom节点,然后调用 mountComponent 方法。我们继续转到src/core/instance/lifecycle.js
看 mountComponent
。
function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el // 需要挂载的 dom 元素赋值给 $el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
}
callHook(vm, 'beforeMount') // 触发 beforeMount 钩子函数
let updateComponent
updateComponent = () => {
// _update 中会调用 patch, _render 会生成 vdom 树
vm._update(vm._render(), hydrating)
}
// updateComponent 会在 Watcher 实例化过程中被调用
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
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
}
如果没有提供 el 和 render 函数,则创建一个空的 vnode, 并触发 beforeMount
钩子。
updateComponent
函数用来比对 virtual dom,并更新dom节点。
接下来新建一个渲染 watcher
,每个组件对应一个渲染 watcher,一个渲染 watcher
对应多个 dep
。watcher 在实例化的过程中,会执行 updateComponent 函数,每个状态的 dep 都会把 watcher 添加到订阅列表中。当用户修改 vm.$data
上的属性时,对应的 dep
会调用 notify
方法,通知订阅的 watchers 依次 update
,也就是更新组件。
总结
vm.$mount(el) 做了什么?
首先 el 必填。
- 如果 vm 提供了 render 函数,优先使用。
- 如果没有 render,再看是否提供了 template, 如果 template 是个选择器符,则获取对应 dom 的 innerHTML,
- 如果 template 不是选择器符,则把它编译成 render 函数。
- 如果 render 和 template 都没有,把 el 选择器对应的 dom 的 outerHTML 作为模版编译成 render 函数。
- 最终,运行 render 函数生成 vnode,交给 vm._update 执行 patch(vnode 的 diff 运算,并挂载到真实 Dom 上)
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!