curl 模拟ajax请求

使用 curl 可以模拟各种http请求:

1
2
3
4
curl -i \
-H "Content-Type: application/json" \
--data '{username:"test010",password:"123456"}' \
http://192.168.1.2:q/login

vue 源码解析三 - Watcher

上一篇讲了对 props 的监听是通过创建 Directive 实现的,而 Directive 内部则创建了一个 Watcher。我们现在来看看 Wacher 是如何监听数据的更新的。

如何关联 props.xxxthis.xxx

watcher.js 中我们看不到任何使用 ES6 相关API来监听属性更新的,反而能看到这么一行代码:

1
2
3
4
5
export default function Watcher (vm, expOrFn, cb, options) {
// ...
vm._watchers.push(this) // 这一行代码
// ...
}

这行代码是在每次创建一个 watcher 实例后都会把这个实例放进 vm._watchers 中保存。这里的 vm 就是 Vue 的一个实例。

那么显然,并不是每一个watcher去自己监听 vm 的数据更新,而是 wachter 通过这种方式把自己注册到 vm 上,当有数据更新的时候,由 vm 负责调用 watcher 来更新数据。

接着上一篇,我们继续看 compileProps 方法,他最后返回的是另一个方法的结果:

1
return makePropsLinkFn(props)

最终调用了 defineReactive 方法:

1
defineReactive(vm, prop.path, value)

这个方法中,会把前面定义的每一个 prop 都在 vm 对象上做一个 reactive 的同名 prop,因此我们才能把 this.show = trueprops.show 关联起来。

这里贴一下简化版的 defineReactive 方法实现,完整版请在 observer/index.js 中查看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
export function defineReactive (obj, key, val) {
var dep = new Dep()
var property = Object.getOwnPropertyDescriptor(obj, key)
var childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val
if (Dep.target) {
// ...
}
return value
},
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val
if (newVal === value) {
return
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = observe(newVal)
dep.notify()
}
})
}

注意这里对闭包的应用,gettersetter 都是操作这个函数闭包中的 val 变量,这个变量的初始值其实就是我们初始化的时候传入的 props 中对应的值。

这样我们就弄清楚了为什么 props.xxx 可以和 vm.xxx(this.xxx) 关联起来。

也能清楚 this.xxx = xxx 是怎么工作的,其实 this.xxx 会调用上面的 set 方法,他会把一个闭包中的变量 val 修改掉,这样下次我们通过 this.xxx 取值的时候,就会取到新的 val 值。

有兴趣的童鞋可以在 getset 方法中下断点,然后修改一下props的值来验证。

到目前为止,我们清楚了 props.xxxthis.xxx 是如何关联的,下一步,我们就要弄清楚,从 this.xxx = xxx 是如何触发 DOM 的更新的。

更新 DOM

先看看上面 defineReactive 中省略的一行关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
get: function reactiveGetter() {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend(); // 关键代码
if (childOb) {
childOb.dep.depend();
}
if (isArray(value)) {
for (var e, i = 0, l = value.length; i < l; i++) {
e = value[i];
e && e.__ob__ && e.__ob__.dep.depend();
}
}
}
return value;
},

其中 dep.depend() 这行代码定义了对这个属性的 依赖,其中 更新DOM 就是一种依赖(另一种依赖就是更新 children.props.xxx)。他最终会把当前这个属性的 watcher 存到 dep.subs 中。具体过程有兴趣的可以断点跟踪一下,过程并不复杂,要注意其中 Deo.target 就是当前属性对应的 watcher

所以在 set 函数中,最后一行 dep.notify() 就会触发对应的 依赖,也就是对 DOM 的更新。

这里说一下大致的流程:

  1. dep.notify() 会触发对应的 wachter.run()
  2. watcher.run() 的时候会触发对应的 directive._update()
  3. 最终由对应的 directive 负责完成DOM更新,比如是一个 textNode 那么最终是由 directives/public/text.js 中的 update 方法完成更新的

vue-props-workflow

注意到这里了吗,前一章说过,对每一个 prop 都会创建一个 directivedirective 内部则创建了一个 watcher 来监听 prop 的更新,一旦监听到更新就会执行 directive.update 来更新 DOM。

大致的流程就是这样的,当然实际上代码比这个复杂很多,这里只是说一个最简单的流程。

vue 源码解析二 - instance

我们从 instance/vue.js 开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Vue (options) {
this._init(options)
}
// install internals
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
miscMixin(Vue)
// install instance APIs
dataAPI(Vue)
domAPI(Vue)
eventsAPI(Vue)
lifecycleAPI(Vue)

上面这段就是 vue.js 中的代码,可以看到对 Vue 这个类的定义包含三部分:

  • _init 函数,做一些初始化操作,如处理父子关系,解析option, 启动HTML解析等
  • mixin 大部分功能的实现都在这里
  • api 全部的接口封装都在这里,而这些接口中部分的实现依然在 mixin

init 函数

init 函数主要做了这么些工作:

  1. 处理组件层级关系: $parent, $chilren, $root
  2. 初始化内部变量:_directives, _events, _isFragment lifecircle 相关标记等
  3. 合并 options:把初始化函数传入的 optionsVue.options 合并,Vue.options 我们后续会在 global 章节详细讲解
  4. 调用其他模块的初始化:_initState, _initEvents
  5. 启动lifecircle: this.$mount

我们这里不会每一行代码都讲到,毕竟像怎么进行 dom 这种并不是vue的核心所在,下面我们重点看两个关键部分:数据绑定模板更新

数据绑定

internal/state.js 中对数据的处理代码如下:

1
2
3
4
5
6
7
Vue.prototype._initState = function () {
this._initProps()
this._initMeta()
this._initMethods()
this._initData()
this._initComputed()
}

也就是 state 包含了五个部分:props, meta, methods, data, computed, 我们以 props 为切入点来看看数据绑定是如何工作的。

props

props 的处理,最终在 src/compiler/compile-props.js 中的 compileProps 方法中实现,这个方法过程如下:

第一步,对 props 做预处理,主要是词法分析和类型检测:

遍历 propOptions 的key,这个 propOptions 就是初始化传入的 props,然后对每一个 key 做如下操作(在循环体中叫name):

  1. 检查name是否是合法的标识符
  2. 根据 name 从 el 上取出绑定的原始值 value,同时会根据 .sync.once 打上对应的标签
  3. 调用 parseDirective 方法解析 value,注意这只是一个简单的词法分析,对 value 做了一个切分,分出其中的 expressionfilter,而这个 expression 依然是一个字符串。注意从这一步开始 valuefilters 被分割开了
  4. 检查 value 是否是一个简单的字面量,如果是一个字面量就会打上一个 optimizedLiteral 标签,后序就不用处理绑定了。字面量就是以字面量形式写出的:true, false, 数字 和 字符串

第一步完成后得到了一个合法的,经过初步处理的 props 数组。然后我们来进行第二步处理.

第二步主要就是把 value 解析出来。

第二步依然是一个循环处理,对第一步得到的 props 数组循环处理,主要是对每一个 prop 创建一个对应的 Directive

  1. 遍历 props 取出一个 prop
  2. _context 上进行绑定,这个 _context 一般就是父组件。
  3. 绑定的过程其实是创建了一个 Directive,这个 Directivedescriptor 如下:
1
2
3
4
5
6
7
8
{
name: 'prop',
prop: prop,
def: {
bind: function,
unbind: function
}
}

其中 bindunbind 是在 directives/internal/prop.js 中定义的。

到这一步我们可以明白了,对每一个 prop 的绑定,其实是创建了一个专门处理 propdirective。那么这个 directive 中显然主要是处理监听的,我们继续往下看。

第三步,在 Directive 内创建 watcher 来监听数据更新。

bind 函数中创建 Watcher 的代码比较简洁,直接贴出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const parentWatcher = this.parentWatcher = new Watcher(
parent,
parentKey,
function (val) {
updateProp(child, prop, val)
}, {
twoWay: twoWay,
filters: prop.filters,
// important: props need to be observed on the
// v-for scope if present
scope: this._scope
}
)
// set the child initial value.
initProp(child, prop, parentWatcher.value)
// setup two-way binding
if (twoWay) {
// important: defer the child watcher creation until
// the created hook (after data observation)
var self = this
child.$once('pre-hook:created', function () {
self.childWatcher = new Watcher(
child,
childKey,
function (val) {
parentWatcher.set(val)
}, {
// ensure sync upward before parent sync down.
// this is necessary in cases e.g. the child
// mutates a prop array, then replaces it. (#1683)
sync: true
}
)
})
}

可以看到,监听 prop 的改动,是通过在 parent 对应的 key 上创建一个 Watcher 来实现的,一旦监听到数据变化就会更新 child 中对应 prop 的值。

如果是双向绑定,那么会在 child 上也对对应的 prop 创建一个 Watcher,当他变动的时候会设置 parent 中对应的值。

Watcher 本身应该也涉及到不少内容,所以对 watcher 的整个工作流程的讲解放到下一章。

看完 props 相关的代码之后就可以得出几个疑惑的结论:

疑问:解析 props 需要分析模板的字符串吗?

答案:不需要分析字符串, this.el 本来就是一个dom,用的就是 Dom 原生提供的 getAttribute 方法,这是 util/dom.jsgetAttr 方法的定义,

1
2
3
4
5
6
7
function getAttr(node, _attr) {
var val = node.getAttribute(_attr);
if (val !== null) {
node.removeAttribute(_attr); // 注意这里,取到值之后就把这个属性给删了,所以我们编译完组件之后就看不到在模板中定义的那些属性了
}
return val;
}

疑问:.sync.once 是怎么解析的?

答案:很简单,假设 props 中定义了一个 a 属性,那么就把 a, a.sync, a.once 这三个 attribute 都尝试获取一下,先取到那个就用哪个。
所以定义了重复的属性会按照代码中的这个顺序覆盖哦, 比如先定义 <x a.sync="1" a="2"> 最终取到的会是 2

1
2
3
4
5
6
7
if ((value = getBindAttr(el, attr)) === null) {
if ((value = getBindAttr(el, attr + '.sync')) !== null) {
prop.mode = propBindingModes.TWO_WAY
} else if ((value = getBindAttr(el, attr + '.once')) !== null) {
prop.mode = propBindingModes.ONE_TIME
}
}

总结下 props 处理的流程

  1. 词法解析,把模板中的原始值做简单的处理,分理出 valuefilter
  2. 创建derctive, 对每一个 prop 创建一个 Directive 负责处理数据更新
  3. 创建watcher, Directive 内部通过创建 Watcher 来实现对数据更新的监听

vue-props-init

如何debug

补充一下如何debug vue 的源码,参考 guide - installDev Build 小节中的方法,自己clone并编译代码,然后在 chrome 开发者工具中按 command + O 打开 vue.common.js,就可以在其中下断点调试。

Next:分析 Watcher 工作流程

vue 源码解析一 - 框架和文件结构

最近一直在埋头写代码,两个开源项目基本占用了全部时间。子曰学而不思则罔,思而不学则殆,无论再忙还是要抽出时间来学习和思考,埋头写代码而不知思考是不会有太大进步的。

在新博客上就以 vue 源码解析系列来开篇吧,因为是边看边写文章的顺序基本是按照自己看源码的顺序来的,效果可能比不上先完整看一遍再写(并不会现有一个对代码结构很透彻的理解再逐步解析每个模块)。

这里我们以 v1.0.26 为基础开始看源码,全部看完一遍以后,会继续看 v2.0 的源码,并比较其中的差异。建议看这篇博客的读者都自行clone源码并切换到 v1.0.26 tag。

基本结构

从入口文件 /src/index.js 入手,顺序捋一遍,vue的大概结构如下:

vue-structure

vue的源码结构分为 InstanceGlobal 两大块,下面我们分别粗略的看看这两个部分

Instance

其中 Instance 就是对 Vue 实例的定义,也就是定义了 Vue 这个类本身,我们通过 new Vue 来生成实例。

Vue 类的定义在 instance 文件夹下,主要包括两方面内容:

  • internal 文件夹下定义的是 Vue 内部的方法,内部方法全部以 _ 开头(所以要避免使用 _ 开头的方法和属性)

    • init 定义了一些初始化操作,比如对 $parent $children 的初始化,$mount, 调用 events 等其他初始化方法
    • events 处理 我们初始化的时候传入的eventswatch 参数
    • lifecircle 实现了从初始化到销毁的整个生命周期
    • state 主要处理 propsdata,定义了对他们的 watcher 来监听数据变动
  • api 文件夹下定义的是实例的接口,是我们可以在实例上调用的方法,全部以 $ 开头

    • data 定义了对数据的操作: $get, $set, $watch, $delete
    • dom 定义了几个常用的dom操作: $before, $after等,还有最常用的 $nextTick 也定义在这里
    • events 定义了事件接口:$on, $emit, $broadcast
    • lifecircle 定义了和生命周期相关的接口:$mount, $compile, $destroy
      -

要注意的是,internalapi 中都涉及到了对 dataevents 的处理,他们是有严格区别的,internal 下都是内部初始化用的,比如如何初始化用户传入的 data ,而api 下面都是提供实例接口的,api 下面定义的接口都不包含具体实现,具体的实现都是在 internal 中的。

Global

Global 的入口文件是 global-api.js,定义了 directives, filters, transition, parser 等全局模块,定义了 Vue 上的静态属性

  • directives 分两类:
    • elementDirectives 只有两个 slotpartical
    • public directives 包含了其余所有的,比如 bind, for, if

instanceglobal-api 最终还依赖了 compiler, observer, parser, utils 等模块。

global 相关的我们留在 instance 后面再看。

下一章,我们从 instance 的整个生命周期入手,来看看一个组件从初始化到销毁都经历了哪些步骤,每个步骤是怎么设计的,重点应该是 数据监听模板渲染 两部分

关于本博客

关于本博客

在CSDN上断断续续写了5年博客,发表了上百篇原创文章,涉及了前端,算法,Java,安卓,Linux 等各种领域。
一直想切换到github上,近期发生了一些不愉快的事情,机缘巧合下也正好把博客搬家到github上。
至于为什么在github上写博客,而不是在CSDN等博客网站上写,大约主要是如下原因:

  • 本人是github重度用户,所有能放在github上基本都不愿意放在别处
  • github 上用markdown写起来更方便
  • 独立博客更自由,可以自定义域名,可以自己设计页面
  • 以前的博客写的很杂,希望换一个地方专心写一些比较高质量的技术博客

此博客源码:https://github.com/lihongxun945/blog
旧的博客地址: http://blog.csdn.net/lihongxun945?viewmode=contents

关于作者@言川

前端工程师,开源爱好者。目前个人开源项目主要有:

  • 微信公众账号开发UI库 jQuery WeUI
  • 正在开发的基于VUE的UI框架 VUM

另外还有一些公司的开源项目就不在这里列出。