Vue.js 是一个渐进式 MVVM 框架,目前被广泛使用,也成为目前前端技术中颇具代表性的一个框架。
按 Vue 作者的说法,Vue(及其生态)是一个渐进式 MVVM 框架,可以按照实际需要逐步进阶使用更多特性
create-compiler.js
)、模板解析(parser
目录)、AST 优化(optimizer.js
)、render()
方法生成(codegen
目录)以及一些其它的辅助代码(比如内置指令相关等)├── codeframe.js 用于出错后定位错误位置
├── codegen 生成编译后的代码
│ ├── events.js
│ └── index.js
├── create-compiler.js 创建编译器
├── directives 生成内置指令代码
│ ├── bind.js
│ ├── index.js
│ ├── model.js
│ └── on.js
├── error-detector.js 用于检查AST是否有错误
├── helpers.js 辅助方法
├── index.js 编译器入口
├── optimizer.js 优化AST,生成静态子树
├── parser 模板解析,将模板解析成AST
│ ├── entity-decoder.js
│ ├── filter-parser.js
│ ├── html-parser.js
│ ├── index.js
│ └── text-parser.js
└── to-function.js 将解析器生成的代码转成函数
instance
目录,用于创建 Vue 实例observer
目录vdom
目录global-api
目录components
目录├── components 内置组件
│ ├── index.js
│ └── keep-alive.js
├── config.js
├── global-api 全局API,用于mixin
│ ├── assets.js
│ ├── extend.js
│ ├── index.js
│ ├── mixin.js
│ └── use.js
├── index.js 入口
├── instance 实例上的成员
│ ├── events.js 事件
│ ├── index.js
│ ├── init.js 初始化方法
│ ├── inject.js 依赖注入
│ ├── lifecycle.js 生命周期函数
│ ├── proxy.js
│ ├── render-helpers 函数辅助函数
│ │ ├── bind-dynamic-keys.js
│ │ ├── bind-object-listeners.js
│ │ ├── bind-object-props.js
│ │ ├── check-keycodes.js
│ │ ├── index.js
│ │ ├── render-list.js
│ │ ├── render-slot.js
│ │ ├── render-static.js
│ │ ├── resolve-filter.js
│ │ ├── resolve-scoped-slots.js
│ │ └── resolve-slots.js
│ ├── render.js render方法
│ └── state.js 应用状态相关
├── observer 响应式数据实现
│ ├── array.js 数组相关实现
│ ├── dep.js 一个观察者
│ ├── index.js
│ ├── scheduler.js 渲染时的调度器
│ ├── traverse.js 遍历
│ └── watcher.js Watcher实现
├── util 一些工具
│ ├── debug.js
│ ├── env.js
│ ├── error.js
│ ├── index.js
│ ├── lang.js
│ ├── next-tick.js
│ ├── options.js
│ ├── perf.js
│ └── props.js
└── vdom 虚拟DOM实现
├── create-component.js
├── create-element.js
├── create-functional-component.js
├── helpers
│ ├── extract-props.js
│ ├── get-first-component-child.js
│ ├── index.js
│ ├── is-async-placeholder.js
│ ├── merge-hook.js
│ ├── normalize-children.js
│ ├── normalize-scoped-slots.js
│ ├── resolve-async-component.js
│ └── update-listeners.js
├── modules
│ ├── directives.js
│ ├── index.js
│ └── ref.js
├── patch.js
└── vnode.js
包含 Vue 与具体平台相关的代码,针对浏览器平台(web
目录)和 weex 平台分别对一些部分进行不同的实现。主要包括:
compiler
目录runtime
目录├── web web平台
│ ├── compiler
│ │ ├── directives
│ │ │ ├── html.js
│ │ │ ├── index.js
│ │ │ ├── model.js
│ │ │ └── text.js
│ │ ├── index.js
│ │ ├── modules
│ │ │ ├── class.js
│ │ │ ├── index.js
│ │ │ ├── model.js
│ │ │ └── style.js
│ │ ├── options.js
│ │ └── util.js
│ ├── entry-compiler.js
│ ├── entry-runtime-with-compiler.js
│ ├── entry-runtime.js
│ ├── entry-server-basic-renderer.js
│ ├── entry-server-renderer.js
│ ├── runtime
│ │ ├── class-util.js
│ │ ├── components 内置组件
│ │ │ ├── index.js
│ │ │ ├── transition-group.js
│ │ │ └── transition.js
│ │ ├── directives 内置指令
│ │ │ ├── index.js
│ │ │ ├── model.js
│ │ │ └── show.js
│ │ ├── index.js
│ │ ├── modules
│ │ │ ├── attrs.js
│ │ │ ├── class.js
│ │ │ ├── dom-props.js
│ │ │ ├── events.js
│ │ │ ├── index.js
│ │ │ ├── style.js
│ │ │ └── transition.js
│ │ ├── node-ops.js
│ │ ├── patch.js
│ │ └── transition-util.js
│ ├── server
│ │ ├── compiler.js
│ │ ├── directives
│ │ │ ├── index.js
│ │ │ ├── model.js
│ │ │ └── show.js
│ │ ├── modules
│ │ │ ├── attrs.js
│ │ │ ├── class.js
│ │ │ ├── dom-props.js
│ │ │ ├── index.js
│ │ │ └── style.js
│ │ └── util.js
│ └── util
│ ├── attrs.js
│ ├── class.js
│ ├── compat.js
│ ├── element.js
│ ├── index.js
│ └── style.js
└── weex Weex平台相关,不看
├── compiler
│ ├── directives
│ │ ├── index.js
│ │ └── model.js
│ ├── index.js
│ └── modules
│ ├── append.js
│ ├── class.js
│ ├── index.js
│ ├── props.js
│ ├── recycle-list
│ │ ├── component-root.js
│ │ ├── component.js
│ │ ├── index.js
│ │ ├── recycle-list.js
│ │ ├── text.js
│ │ ├── v-bind.js
│ │ ├── v-for.js
│ │ ├── v-if.js
│ │ ├── v-on.js
│ │ └── v-once.js
│ └── style.js
├── entry-compiler.js
├── entry-framework.js
├── entry-runtime-factory.js
├── runtime
│ ├── components
│ │ ├── index.js
│ │ ├── richtext.js
│ │ ├── transition-group.js
│ │ └── transition.js
│ ├── directives
│ │ └── index.js
│ ├── index.js
│ ├── modules
│ │ ├── attrs.js
│ │ ├── class.js
│ │ ├── events.js
│ │ ├── index.js
│ │ ├── style.js
│ │ └── transition.js
│ ├── node-ops.js
│ ├── patch.js
│ ├── recycle-list
│ │ ├── render-component-template.js
│ │ └── virtual-component.js
│ └── text-node.js
└── util
├── element.js
├── index.js
└── parser.js
服务端渲染的代码。包括创建服务端渲染包、服务端针对渲染的特殊处理等等
├── bundle-renderer
│ ├── create-bundle-renderer.js
│ ├── create-bundle-runner.js
│ └── source-map-support.js
├── create-basic-renderer.js
├── create-renderer.js
├── optimizing-compiler
│ ├── codegen.js
│ ├── index.js
│ ├── modules.js
│ ├── optimizer.js
│ └── runtime-helpers.js
├── render-context.js
├── render-stream.js
├── render.js
├── template-renderer
│ ├── create-async-file-mapper.js
│ ├── index.js
│ ├── parse-template.js
│ └── template-stream.js
├── util.js
├── webpack-plugin
│ ├── client.js
│ ├── server.js
│ └── util.js
└── write.js
初始化生命周期
初始化事件绑定
初始化 Render
调用钩子 beforeCreate
初始化依赖注入 Injections
初始化状态 State
初始化依赖注入 Provide
调用钩子 created
.el
属性,则调用 $mount(el)
Vue 使用 getter/setter 机制实现了数据变更的自动监测。再深入思考一下这个问题,为什么需要数据变更的监测?是因为我们不希望手工去更新 DOM 元素,而是希望数据的任何变更都能自动反映到 DOM 的变更中,而这个过程中,依赖收集是一个必不可少的过程
假设有 a
、b
、c
3 个值,它们之间存在依赖关系,即 a
的变化会导致 b
的变化,而 b
的变化又会导致 c
的变化。
当 a
发生变化时,c
应该如何处理,通常来讲,此时有两种策略:推送(push)、拉取(pull)。
拉取(pull)的含义是指,当 c
被访问的时候,会去寻找 b
的最新值,而 b
被访问时又会去寻找 a
的最新值。因此当 a
发生变化时,它只需要管理自己的变更即可,其他依赖它的值在被访问到的时候都会自动拉取一遍最新值,从而完成数据依赖的更新。
推送(push)则是指,当 a
发生变化时,需要主动通知 b
进行更新,而 b
更新时又需要通知 c
更新。从而完成数据依赖的更新。
// 推送(push) a变化 ----> b变化 ----> c变化
// 拉取(pull) c被访问 ----> b被访问 ----> a被访问
Vue 中与数据有关的概念大致有这样几类:
data、props、computed
watch
methods 以及生命周期方法(created、mounted 等)
对第一类数据,它们都可以直接在 Vue 的实例 vm
上进行访问,也可以直接在模板中进行访问。以模板为例,当模板中引用了一个这样的数据时,如果数据发生变更,需要直接反映到对应的 DOM 元素中。而由于原理限制(DOM 元素会一直显示,并且不会主动重新渲染),数据无法被 DOM 主动重新访问,因此此类数据的依赖更新只能采用 “推送(push)”。
对 watch
而言,与第一类数据类似,不同的是它的使用方式是一个回调函数,例如
watch: {
// 当foo的值发生变化时,调用callback()函数
foo: function callback(){
// 访问foo的值,做些其它事情
}
}
在这种情况下,callback()
也无法主动运行,因此不能采用 “拉取(pull)” 的策略,只能采用 “推送(push)” 策略,即 foo
的值发生变化时,主动调用 callback()
函数。
其他的像 methods
中的方法以及生命周期方法,都可以直接通过 this.xxx
的方式读取数据,因此可以直接采用 “拉取(pull)” 的依赖更新策略。
因为 “拉取(pull)” 策略实际上就是正常的变量访问,如果有依赖关系都会顺着依赖定义的地方自动进行计算,因此不需要 Vue 进行重点关注。而 “推送(push)” 策略则不同,它需要关注每一个变量变更的时候,有哪些地方依赖这个变量,并一一通知这些地方进行更新。所以 Vue 中的依赖收集主要关注的就是采用 “推送(push)” 策略进行依赖更新的地方,即
data、props、computed
watch
如前所述,当数据变化以后,需要更新的地方主要包括computed
定义的变量,以及watch
。Vue 组件被挂载(mount
)时,会针对每个这样的地方,初始化一个Watcher
。Watcher
会记住这个表达式或者函数(Vue 允许开发者watch
一个函数),并暴露一个名为update()
的方法,用来给外界调用。一旦这个方法被调用,就表示 “你这个 Watcher 所依赖的数据有更新,麻烦对对应的模板进行更新 / 麻烦调用回调函数”
模板中的表达式也需要更新,但这里 Vue 采用的策略是不精准地对应依赖关系,而是在需要的时候将模板全部重新渲染一遍(使用虚拟 DOM 减少真实的渲染工作量),因此模板中的表达式不需要收集依赖
那数据变更的时候是如何知道应该要调用哪些 Watcher
呢,又是在什么时候调用 Watcher
进行更新的呢?
这就要回到我们在前文中反复提到的 getter
/setter
机制,我们知道 Vue 使用这一机制来进行依赖收集,但前文中并未说明具体是如何处理的,接下来我们就来揭开这一机制的神秘面纱。
在 Vue 实例初始化的时候,会将我们传递给组件的 data
(确切地说,是 data()
方法的返回值)进行转换,由纯对象转换成 getter/setter
的定义,这一过程主要靠 defineReactive()
方法。在这个方法中,Vue 会对传入对象的每一个属性定义一个 getter
和一个 setter
。这样,每当这个属性被访问的时候,getter
就会被调用。而每当这个属性被更新的时候,setter
就会被调用。
每个数据属性除了有 getter
之外,还有一个对应的 Dep
类的实例 dep
(它是一个观察者模式的实现,可以先简单理解为一个依赖列表),当 getter
调用时,会判断当前是否有 Watcher
正在进行依赖收集,如果是的话,就记录 Dep
实例与 Watcher
的依赖关系,从而完成依赖收集。这个判断的详细过程如下:
Watcher
,Watcher
会将自己挂到 Dep.target
(静态成员)上,表示当前正在进行收集依赖的正是刚刚建立的 Watcher
;Watcher
的表达式,进行一次求值运算。因为这次求值运算是主动调用的,因此它所有的依赖都会被一一进行取值运算(依赖更新的 “拉取(pull)” 策略);getter
会被调用,如果发现 Dep.target
存在,则表示当前有 Watcher
正在进行依赖收集,此时 getter
会调用 dep
实例对象的 depend
方法,建立当前属性值与 Dep.target
的关联,从而完成整个收集依赖的工作。整个依赖收集过程最关键的入口在于core/observer/index.js
第 135 行defineReactive()
方法,这个方法接受两个参数,分别是obj
和key
,表示将实例上数据obj[key]
转换为 getter/setter,以便可以响应数据变化。这个方法简化后大体结构如下:
export function defineReactive (
obj: Object,
key: string,
) {
// 针对这个obj和key定义一个Dep实例
const dep = new Dep()
// 一些其他的判断,省略
// ...
// 在obj上定义key的getter/setter
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// getter定义,当访问obj[key]时被调用
get: function reactiveGetter () {
// 先取值
const value = getter ? getter.call(obj) : val
// 判断是否有Watcher正在进行依赖收集
// 如果有的话,调用dep.depend(),表示“我被调用了,它依赖我,请记录”
// 因为组件挂载时会新建一个Watcher,Watcher会调用求值,因此在挂载的时候,Dep.target是存在的
if (Dep.target) {
// Dep.target依赖了dep
dep.depend()
// 嵌套处理和数组处理,省略
// ...
}
// 返回值
return value
},
set: function reactiveSetter (newVal) {
// 取原值
const value = getter ? getter.call(obj) : val
// 如果新值原值一样,不处理
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
// 一些判断,省略
// ...
// 赋新值
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 告诉所有依赖我的Watcher,我变化了,请更新
dep.notify()
}
})
}
结合代码上的注释,能很明显看到,getter
进行依赖收集、setter
在值发生变动后通知依赖进行更新的过程
在 core/instance/state.js
中,首先定义了一个名为 proxy()
的方法,它的作用是用来代理 vm
上属性名的访问。
例如当我们访问 vm.propKey
这个属性时,实际上是访问 vm._props.propKey
,这个代理的过程就是 proxy()
方法干的事情
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
还是举上面的例子,访问 vm.propKey
属性需要代理到 vm._props.propKey
,那么调用时就是这样 proxy(vm, '_props', 'propKey')
。
在下面,我们将看到,我们声明的 data
、props
、computed
等成员中的属性,都不是直接挂在 vm
对象上的,但我们却可以直接用 vm.xxx
这样的方式(在代码中更多写作 this.xxx
)来访问,就是因为这个 proxy()
将访问过程进行了代理
Vue 专门写了一个类Watcher
来处理数据监听,它一般会跟随一些属性一起出现,这些属性可能是computed
或者data
或者props
等等,当这些属性依赖的别的属性发生变化时,由Watcher
实例来执行需要变更的具体逻辑
initState 定义vm._watchers
,接下来一一初始化所有状态相关的属性,包括props
/methods
/data
/computed
/watch
initProps()
在 initProps()
中,正是调用的 defineReactive(props, key, value)
方法来讲属性转换为响应式对象的。现在我们可以将整个逻辑串起来了:
toggleObserver(false)
,此后 observe()
不会将传入的对象转换为响应式对象observe()
将值转换成响应式对象,但是因为第 1 步操作,这个转换不会进行toggleObserver(true)
,恢复 observe()
将对象转换为响应式对象的逻辑总结下来,上面做的事情就是一句话:不要将属性值做 “对象转响应式对象” 的转换
initData()
看完属性的处理之后,数据的处理逻辑就显得特别简单直接了:
data()
方法获取数据值(Vue 推荐 data
写成一个函数来返回值,但源码中也处理了 data
不是函数的情况)methods
、props
重名vm.xxx
的访问代理到 vm._data.xxx
observe(data, true)
,使整个数据对象变成响应式对象computed
Watcher
,设置属性 lazy
为 true
Watcher
加入依赖列表中update()
方法将 dirty
置为 true
dirty
为 true
,调用 get()
方法获取新值,并将 dirty
设置为 false
,完成整个读取值和缓存的过程watch
$watch() 方法被定义在 Vue 的原型上,它的逻辑简单直接:
针对传入的表达式或函数创建一个 Watcher
如果 immediate 为 true,则立马调用回调函数 cb 一次
返回取消监听的方法 unwatchFn
总结:我们详细了解了 Vue 数据监听的实现原理。以上一节介绍的依赖收集为核心机制,Vue 将它运用到了计算属性、数据、监听器等各种属性的处理上,为了实现这一过程,Vue 还定义了proxy()
方法和Watcher
类。从而让开发者能真正使用一款 “响应式” 的前端框架来完成应用开发
整体而言,Vue 的处理方式大致分为几步:
其中第一步和第二步由 render()
方法来完成,第 3 步由 mount()
方法来完成。
Vue 编译模板的过程:
compiler
前文提过,Vue 在将模板编译为 AST 并且优化之后,会将 AST 转换成虚拟 DOM 树(即 VNode 树)。这个过程就是render()
方法来完成的。
Vue.component('anchored-heading', {
render: function (createElement) {
return createElement(
'h' + this.level, // 标签名称
this.$slots.default // 子节点数组
)
},
props: {
level: {
type: Number,
required: true
}
}
})
使用render()
方法而不使用模板的一个好处是带来更大的灵活性,像上面这个例子,可以根据传入的level
参数动态决定需要渲染的元素。
render() 方法的原理是它可以由模块编译生成,也可以直接传入使用,而它的实质就是使用 createElement() 方法生成一棵虚拟 DOM 树
codegen 的逻辑就是读取 AST,然后将对应的元素、属性、子元素等信息都遍历一遍,生成了一个 render() 函数。在后续组件进行挂载时,render() 方法会被调用,此时就会生成整个虚拟 DOM
回顾一下,Vue 实例在经历初始化后,完成了很多事情,如依赖收集、数据监听、模板编译、生成 render() 方法等等。最终,Vue 实例还是要将其代表的逻辑渲染到真实的 web 页面上。
这个过程就需要调用 render() 方法,首先生成完整的虚拟 DOM 树,然后将虚拟 DOM 树挂载到 web 页面上
首先,针对 web 平台,Vue 为实例增加了一个$mount
方法。它的定义在 platforms/web/runtime/index.js
中,本质上是调用了 mountComponent()
方法。
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
关于挂载的部分,它做了这样几件事情:
updateComponent()
方法,这个方法做的事情就是先获取 vm._render()
的结果(即 render()
方法返回的虚拟 DOM),然后调用 vm._update()
Watcher
,它监听的表达式是 updateComponent()
方法,在进行依赖收集的时候,updateComponent()
被调用,即组件被完全重新渲染,此时就能收集到 vm 中所有的依赖,简单说就是 vm 在渲染时用到的任何依赖发生变化时都会触发这个 Watcher
进行更新vm._update() 负责将虚拟 DOM 渲染到真实的 DOM 中,该方法的第一个参数是组件的虚拟 DOM。它的逻辑并不复杂,核心逻辑只有下面几句:
const prevVnode = vm._vnode
vm._vnode = vnode
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
这段代码首先判断了 vm._vnode 是否存在,如果不存在,则说明这个组件是初次渲染,否则说明之前渲染过,这一次渲染是需要进行更新。针对这两种情况,分别用不同的参数调用了__patch__() 方法:
如果是初次渲染,第一个参数是真实的 DOM 元素
如果不是初次渲染,第一个参数是前一次渲染的虚拟 DOM
createElm()
createElm()
执行过程中,如果发现某个虚拟 DOM 节点是组件,则会调用 createComponent()
createComponent()
调用 init
hook,生成组件对应的 Vue 实例适当拆解一下.vue
组件,会发现它的解析其实是在 JS 文件打包的时候(通过 webpack 的 vue-loader 或者类似的工具),将.vue
文件解析成为 js 文件。而解析的过程从原理上讲则简单明了:
<template> 部分被模板解析、生成 AST,最后生成 render() 方法,成为组件对象的一部分
<script> 几乎不做处理,直接被导出使用
<style> 是纯工程化的处理方式,最终被动态写入 HTML 中,或者在打包的时候被单独提取成 CSS 文件
Vue 会最终将组件的各种声明都放到vm.$options.components
中,供渲染时引用。在渲染时组件也拥有独立的 Vue 实例,在父实例渲染的时候只会生成一个占位虚拟 DOM,组件的渲染则由组件自行完成
将双向绑定拆开来看,有两个方向的变化需要处理:
第 1 个方向其实和普通的模板数据渲染没有什么区别,这一点在之前已经进行过比较详细的分析,因此这里就不再详述。这里重点关注第 2 个点的实现。
在 Vue 中,双向绑定是通过 v-model 指令来实现的,但是这个指令在 1.0 和 2.0 中的实现原理差别比较大。从 Vue 2.0 开始,v-model 变成了一个语法糖,本质上相当于:value 的绑定 + @input/@change 事件绑定。
一些基本的常识:
前面我们说过 Vue 是数据驱动界面的,当数据发生变动时,Vue 会通过 VNode 的渲染去更新 DOM,而这个过程上本质还是使用 DOM API 修改 DOM。按上面的常识,JS 操作完 DOM 后,DOM 的渲染、更新是在微任务中的。也就是说,如下的代码是取不到更新后的 DOM 的(来自 Vue 官方文档):
vm.message = 'new message' // 更改数据
vm.$el.textContent === 'new message' // false
因为此时 DOM 还没有更新,直接读取 DOM 返回的仍然是更新前的信息。此时我们就需要异步去读取。因此 Vue 提供了 nextTick()
方法来处理这种情况:
vm.message = 'new message' // 更改数据
vm.$el.textContent === 'new message' // false
Vue.nextTick(function () { vm.$el.textContent === 'new message' // true })
除此之外,在组件 mounted
生命周期事件发生时,Vue 实际上并未完成子组件的渲染,因此通过 this.$refs
来获取子组件也是无法获取到的。此时也需要通过 nextTick()
方法来异步读取
JS 在执行完宏任务后,会获取所有的微任务并一一执行,其中 DOM 更新也属于这些微任务中的一员。因此,如果nextTick()
能够将回调函数安排到微任务中,将比安排到宏任务中更快被执行。Vue 的nextTick()
实现正是这样一种思路:尽量将任务安排到微任务中,如果实在是不支持,则采用一些方法作回退,确保回调函数能被执行(即使是被安排到宏任务执行)
因为 Vue 会运行在各种不同的环境中,而各个环境的能力并不完全一样,所以这段逻辑中,Vue 分了很多情况来分别处理:
第 1 种情况,有原生 Promise,则使用 Promise 的.then()
方法来安排异步任务,因为原生 Promise 的任务会被安排到微任务中:
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
}
isUsingMicroTask = true
}
第 2 种情况,没有原生 Promise,也不是 IE,并且 MutationObserver
可用,则会使用它,它是一个原生的 DOM 方法,可以在 DOM 元素发生变更时调用指定的回调。Vue 会创建一个 DOM 节点(文本节点),并修改它的属性为 0
或 1
(counter = (counter + 1) % 2
),此时 MutationObserver
会观察到 DOM 节点发生变化,触发回调调用 flushCallbacks()
。值得注意的是,MutationObserver
的回调任务也是被安排到微任务中:
else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
}
如果以上情况都不满足,则 Vue 会分别尝试用 setImmediate()
和 setTimeout()
来安排任务,此时任务会被安排到宏任务中:
else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
一个典型的前端路由在单页面场景下大概需要关注以下几个问题:
一般来说,1 是纯计算逻辑,不需要什么特别的处理,2 可以由location
这个 API 进行获取,因此前端路由中值得关注的核心问题主要就是 3 和 4,简单地归纳就是更新浏览器 URL 和监视浏览器 URL 改变
修改 URL 中的 hash 部分。如/home#/hello/world
,其中的 hash 部分就是#/hello/world
,这部分在浏览器导航的时候并不会被传给后端服务器,也可以方便地用 JavaScript 修改,并且修改它时也不会发生重新导航的情况
除了需要更新 URL 以外,路由还需要能够监视 URL 的变更。在这个问题上,因为浏览器的能力不尽相同,有 3 种不同的方案:
hashchange
事件,直接监听这个事件即可popstate
事件,这个事件与下文 history 模式有关,留到下面介绍hash 模式使用虽然方便,但是有两个比较明显的问题:
浏览器厂商和标准组织为这一场景给出了另一种更好的解决方案,即 vue-router 中支持的 history 模式。
这个模式的核心在于 history.pushState(state, title, url) 这个 API,它的含义是向浏览器的历史栈(即前进后退的栈)中压入一个新的状态,从逻辑上相当于跳转到了一个新的页面,但是并不真的重新加载或重新导航。在单页面应用的场景中,可以使用这个 API 很方便地修改浏览器中的 URL,并正确地处理前进 / 后退的问题。
那我们要如何监视 URL 的修改呢?浏览器为我们提供了 popstate 事件。当用户进行导航动作(前进 / 后退等)或有 history.back()、history.forward() 之类的调用时,popstate 事件就会发生。因此只要监听这个事件,就能获取浏览器 URL 可能发生了改变。
如果我们使用 history 模式的前端路由,当前端的界面发生变化时,对应的 URL 也会发生变化,此时 URL 是由前端逻辑负责写入的,例如 /hello/vue
。如果此时用户刷新了页面,或者将这个 URL 分享给了其它人,则对 /hello/vue
这个路由的访问会首先到达后端服务器,如果后端服务器不能正确处理这个地址的访问,就可能出现 404 的错误。
如果你在开发的时候发现一切正常,但一刷新页面就会 404,需要回到首页的地址才能访问,那么基本上就可以确定是因为后端没有办法正确处理由前端路由写入的 URL 而导致的问题。
这个问题的解决方法也比较简单,即让后端能正确地处理前端逻辑写入的 URL。因为前端只有一个页面,因此后端不论用户访问的 URL 是什么,只要碰到由前端路由负责控制的 URL,就统一返回唯一的一个页面的 HTML 即可
vue-router 的使用主要有几个步骤:
beforeCreate()
mixin,将_route
数据变成响应式数据History
,并进行初次路由匹配_route
触发重新渲染这样做会带来几个好处:
状态数据用 State 表示,整个流程是这样的:
dispatch()
方法调用一个 Action;commit()
方法,调用一个 Mutation;vue-cli 是 Vue 官方提供的命令行工具,它具有许多功能,如:
可以在命令行中直接使用 vue
命令来使用它。
这样的用法是利用了 npm 提供的机制,它需要开发者在 package.json
中的 bin
中指定命令的名称和命令对应的.js
文件。当模块被安装的时候,npm 会自动在全局或者 node_modules/.bin
下生成命令行脚本。
@vue/cli
的 package.json
中是这样声明的:
{
"name": "@vue/cli",
"version": "4.5.11",
"bin": {
"vue": "bin/vue.js"
}
}
上面的代码声明了一个 vue
命令,当它被调用时执行 bin/vue.js
中的代码。因此 bin/vue.js
就是 @vue/cli
的命令行入口文件。
bin/vue.js 作为命令行的入口文件,主要功能是处理命令的输入和解析。为了更方便地处理命令行输入的命令和参数解析,引用了 commander 模块。
整个文件比较长,但是结构是比较简单的,大部分的代码都在编写每个命令的参数格式和说明。
const program = require('commander')
const loadCommand = require('../lib/util/loadCommand')
program
.version(`@vue/cli ${require('../package').version}`)
.usage('<command> [options]')
// create命令
program
.command('create <app-name>')
.description('create a new project powered by vue-cli-service')
.option('-p, --preset <presetName>', 'Skip prompts and use saved or remote preset')
.option('-d, --default', 'Skip prompts and use default preset')
.action((name, cmd) => {
require('../lib/create')(name, options)
})
// 省略其它命令
// serve命令
program
.command('serve [entry]')
.description('serve a .js or .vue file in development mode with zero config')
.option('-o, --open', 'Open browser')
.option('-c, --copy', 'Copy local url to clipboard')
.option('-p, --port <port>', 'Port used by the server (default: 8080 or next available port)')
.action((entry, cmd) => {
loadCommand('serve', '@vue/cli-service-global').serve(entry, cleanArgs(cmd))
})
// 省略其它命令
program.parse(process.argv)
if (!process.argv.slice(2).length) {
program.outputHelp()
}
参考: vuejs/vue vuejs/vue-next https://www.imooc.com/read/89/article/2466