参考文章:http://hcysun.me/2017/03/03/Vue%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0/?utm_source=qq&utm_medium=social&utm_member=ZTZmNWZkYzIzMmQ2MjA0ZDNjNTA3ZjdhNDA2MjAzNDQ%3D%0A#%E4%B8%80%E3%80%81%E4%BB%8E%E4%BA%86%E8%A7%A3%E4%B8%80%E4%B8%AA%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE%E5%85%A5%E6%89%8B

vue3.0 尝鲜 -- 摒弃 Object.defineProperty,基于 Proxy 的观察者机制探索

Vue.js 源码:https://github.com/vuejs/vue

Vue技术内幕:http://hcysun.me/vue-design/

源码分析文档:https://ustbhuangyi.github.io/vue-analysis/

1、知识点

2、https://ustbhuangyi.github.io/vue-analysis/prepare/flow.html

Flow 是facebook出品的JavaScript静态类型检查工具。Vue.js的源码就利用Fow做了静态类型检测

为什么用Flow?

因为JavaScript是动态类型语言(弱类型语言),虽然灵活,但是也容易出现隐患代码。

类型检查是当前动态类型语言的发展趋势,所谓类型检查,就是在编译期尽早发现(由类型错误引起的)bug,又不影响代码运行(不需要运行时动态检查类型),使编写 JavaScript 具有和编写 Java 等强类型语言相近的体验。

Flow的工作方式

  • 类型推断:通过变量的使用上下文来推断出变量类型,然后根据这些推断来检查类型。

 /*@flow*/``

 function split(str) {
return str.split(' ')
} split()

Flow 检查上述代码后会报错,因为函数 split 期待的参数是字符串,而我们输入了数字。

类型注释

  • 类型注释:事先注释好我们期待的类型,Flow 会基于这些注释来判断

 /*@flow*/

 function add(x: number, y: number): number {
return x + y
} add('Hello', )

现在 Flow 就能检查出错误,因为函数参数的期待类型为数字,而我们提供了字符串。

Flow还支持一些常见的类型注释:数组、类和对象、Null

3、Vue.js源码目录设计    https://ustbhuangyi.github.io/vue-analysis/prepare/directory.html

Vue.js 的源码都在 src 目录下,其目录结构如下:

 src
├── compiler # 编译相关
├── core # 核心代码
├── platforms # 不同平台的支持
├── server # 服务端渲染
├── sfc # .vue 文件解析
├── shared # 共享代码

从 Vue.js 的目录设计可以看到,作者把功能模块拆分的非常清楚,相关的逻辑放在一个独立的目录下维护,并且把复用的代码也抽成一个独立目录。

这样的目录设计让代码的阅读性和可维护性都变强,是非常值得学习和推敲的。

4、源码构建 https://ustbhuangyi.github.io/vue-analysis/prepare/build.html#构建脚本

主要包括构建脚本和构建过程

Runtime Only VS Runtime + Compiler的区别

5、new Vue的时候,会把传入的option都绑定到$options上,所以我们可以通过vm.$options或者this.$options获取传入的参数(如:el,data)

6、new Vue()的时候发生了什么?

地址:https://ustbhuangyi.github.io/vue-analysis/data-driven/new-vue.html

new 关键字在 Javascript 语言中代表实例化是一个对象,而 Vue 实际上是一个类,类在 Javascript 中是用 Function 来实现的,来看一下源码,在src/core/instance/index.js 中。

会调用_init函数进行初始化。会初始化生命周期、事件、props、data、methods、computed与watch等。

最主要的是通过Object.defineProperty设置setter与getter函数,用来实现【响应式】与【依赖收集】

7、Vue 实例挂载的实现

Vue 中我们是通过 $mount 实例方法去挂载 vm 的

8、Vue 的 _render 方法是实例的一个私有方法,它用来把实例渲染成一个虚拟 Node。它的定义在 src/core/instance/render.js 文件中:

9、其实 VNode 是对真实 DOM 的一种抽象描述,它的核心定义无非就几个关键属性,标签名、数据、子节点、键值等,其它属性都是都是用来扩展 VNode 的灵活性以及实现一些特殊 feature 的。由于 VNode 只是用来映射到真实 DOM 的渲染,不需要包含操作 DOM 的方法,因此它是非常轻量和简单的。

Virtual DOM 除了它的数据结构的定义,映射到真实的 DOM 实际上要经历 VNode 的 create、diff、patch 等过程。那么在 Vue.js 中,VNode 的 create 是通过之前提到的 createElement 方法创建的

10、Vue.js 利用 createElement 方法创建 VNode,它定义在 src/core/vdom/create-elemenet.js 中:

分为俩部分:children 的规范化和VNode 的创建

那么至此,我们大致了解了 createElement 创建 VNode 的过程,每个 VNode 有 childrenchildren 每个元素也是一个 VNode,这样就形成了一个 VNode Tree,它很好的描述了我们的 DOM Tree。

11、Vue 的 _update 是实例的一个私有方法,它被调用的时机有 2 个,一个是首次渲染,一个是数据更新的时候;由于我们这一章节只分析首次渲染部分,数据更新部分会在之后分析响应式原理的时候涉及。_update 方法的作用是把 VNode 渲染成真实的 DOM,它的定义在 src/core/instance/lifecycle.js 中:

12、初始化 Vue 到最终渲染的整个过程。

从new Vue开始,先执行init,进行一堆初始化操作;然后进行挂载,执行$mount

$mount的执行过程:如果是带编译版本的(就是template这样的,我们一般写的那种),就执行compile,生成render function;如果直接就是render function

得到render function后,调用render方法,生成vnode,之后通过patch过程,生成真实的DOM

备注:在vue中,会把之前写的真实的dom删掉,然后通过Virtual DOM重新生成真实的dom

13、

14、

知道了 3 种异步组件的实现方式,并且看到高级异步组件的实现是非常巧妙的,它实现了 loading、resolve、reject、timeout 4 种状态。异步组件实现的本质是 2 次渲染,除了 0 delay 的高级异步组件第一次直接渲染成 loading 组件外,其它都是第一次渲染生成一个注释节点,当异步获取组件成功后,再通过 forceRender 强制重新渲染,这样就能正确渲染出我们异步加载的组件了。

15、

defineReactive 函数最开始初始化 Dep 对象的实例,接着拿到 obj 的属性描述符,然后对子对象递归调用 observe 方法,这样就保证了无论 obj 的结构多复杂,它的所有子属性也能变成响应式的对象,这样我们访问或修改 obj 中一个嵌套较深的属性,也能触发 getter 和 setter。

16、 Vue 会把普通对象变成响应式对象,响应式对象 getter 相关的逻辑就是做依赖收集

Vue为数据中的每一个key维护一个订阅者列表。对于生成的数据,通过Object.defineProperty对其中的每一个key进行处理,主要是为每一个key设置getset方法,以此来为对应的key收集订阅者,并在值改变时通知对应的订阅者。

最开始new Vue(),初始化代码的时候,就会进行依赖收集,之后改变值以后,会再次进行依赖收集

参考网站:https://www.jianshu.com/p/e6e1fa824849

17、计算属性 computed的优化有俩处:  参考:https://ustbhuangyi.github.io/vue-analysis/reactive/computed-watcher.html#computed

(1)、当初始化computed的时候,会先进行收集依赖,但不会触发watch,只有当渲染到computed计算的属性时,就触发了计算属性的 getter,它会拿到计算属性对应的 watcher,然后执行 watcher.depend()

(2)、一旦我们对计算属性依赖的数据做修改,则会触发 setter 过程,通知所有订阅它变化的 watcher 更新,执行 watcher.update() 方法

记住这里:getAndInvoke 函数会重新计算,然后对比新旧值,如果变化了(就算依赖一直变化,只要最终值没有,那就不会触发回调函数)则执行回调函数,那么这里这个回调函数是 this.dep.notify(),在我们这个场景下就是触发了渲染 watcher 重新渲染。

18、

响应式原理图

参考:https://segmentfault.com/q/1010000010977528

对上面的理解:页面中所用到的数据data,渲染的时候就会触发Object.defineReactive的getter方法

getter方法会将window.target(watch,每个用到这个数据的地方,都会生成一个watch)保存到subs数组中

subs数组抽取出来,封装到Dep类中,getter中要new Dep(),调用Dep类中的addSub,将watch保存到subs数组中

数据改变的时候,就会触发setter方法==》触发Dep类中的notify方法==》遍历subs数组==》触发watch中的update方法

综合:每一个使用的数据都有一个单独的Dep类,每个Dep类中有多个watch(根据使用此数据来生成,一个地方使用就生成一个watch)

19、编译入口逻辑之所以这么绕,是因为 Vue.js 在不同的平台下都会有编译的过程,因此编译过程中的依赖的配置 baseOptions 会有所不同。而编译过程会多次执行,但这同一个平台下每一次的编译过程配置又是相同的,为了不让这些配置在每次编译过程都通过参数传入,Vue.js 利用了函数柯里化的技巧很好的实现了 baseOptions 的参数保留。同样,Vue.js 也是利用函数柯里化技巧把基础的编译过程函数抽出来,通过 createCompilerCreator(baseCompile) 的方式把真正编译的过程和其它逻辑如对编译配置处理、缓存处理等剥离开,这样的设计还是非常巧妙的。

parse 的目标是把 template 模板字符串转换成 AST 树,它是一种用 JavaScript 对象的形式来描述整个模板。那么整个 parse 的过程是利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的回调函数,来达到构造 AST 树的目的。

AST 元素节点总共有 3 种类型,type 为 1 表示是普通元素,为 2 表示是表达式,为 3 表示是纯文本。

optimize 的过程,就是深度遍历这个 AST 树,去检测它的每一颗子树是不是静态节点,如果是静态节点则它们生成 DOM 永远不需要改变,这对运行时对模板的更新起到极大的优化作用。

我们通过 optimize 我们把整个 AST 树中的每一个 AST 元素节点标记了 static 和 staticRoot,它会影响我们接下来执行代码生成的过程。

20、event事件

注意:vm.$emit 是给当前的 vm 上派发的实例,之所以我们常用它做父子组件通讯,是因为它的回调函数的定义是在父组件中,对于我们这个例子而言,当子组件的 button 被点击了,它通过 this.$emit('select') 派发事件,那么子组件的实例就监听到了这个 select 事件,并执行它的回调函数——定义在父组件中的 selectHandler 方法,这样就相当于完成了一次父子组件的通讯。

21、v-model实际是一个语法糖

<input v-model="message">
<input :value="message" @input="message=$event.target.value">

这是一回事

v-model总结:

v-model是一个语法糖

在input中  这俩个是相等的

<input v-model="searchText">

等价于

<input :value="searchText" @input="searchText = $event.target.value">

当然,Vue还做了一些优化

在组件中

https://cn.vuejs.org/v2/guide/components.html#在组件上使用-v-model

<test v-model="searchText"></test>

等价于

<test :value="searchText" @input="searchText = $event"></test>

在test组件里,要这样写

<template>
<div class="test">
<input type="checkbox" :checked="value" @change="$emit('input', $event.target.checked)"> // 使用$emit向父组件传回input事件
</div>
</template> <script>
export default {
name: 'test',
props: ['value'] // 这个必须有,而且默认传过来的就是value
}
</script>

在实际中,我们传递value和input有可能会照成歧义,这是我们可以自定义父组件传过来的值和向父组件传递的事件

<template>
<div class="test">
<input type="checkbox" :checked="checked" @change="$emit('changeChecked', $event.target.checked)">
</div>
</template> <script>
export default {
name: 'test',
model: {
prop: 'checked',
event: 'changeChecked'
},
props: ['checked'] // 这个必须有,值就是我们上面定义的prop
}
</script>

22、slot

普通插槽是在父组件编译和渲染阶段生成 vnodes,作为当前组件渲染vnode的children,所以数据的作用域是父组件实例,子组件渲染的时候直接拿到这些渲染好的 vnodes

作用域插槽,父组件在编译和渲染阶段并不会直接生成 vnodes,而是在父节点 vnode 的 data 中保留一个 scopedSlots 对象,存储着不同名称的插槽以及它们对应的渲染函数,只有在编译和渲染子组件阶段才会执行这个渲染函数生成 vnodes,由于是在子组件环境执行的,所以对应的数据作用域是子组件实例。

两种插槽的目的都是让子组件 slot 占位符生成的内容由父组件来决定,但数据的作用域会根据它们 vnodes 渲染时机不同而不同。

23、keep-alive

<keep-alive> 组件是一个抽象组件,它的实现通过自定义 render 函数并且利用了插槽,并且知道了 <keep-alive> 缓存 vnode,了解组件包裹的子元素——也就是插槽是如何做更新的。且在 patch 过程中对于已缓存的组件不会执行 mounted,所以不会有一般的组件的生命周期函数但是又提供了 activated 和 deactivated 钩子函数。另外我们还知道了 <keep-alive> 的 props 除了 include 和 exclude 还有文档中没有提到的 max,它能控制我们缓存的个数。

24、

25、vue-touter

参考网站:https://github.com/DDFE/DDFE-blog/issues/9

导航守卫的参考:https://www.cnblogs.com/le220/p/10162689.html

完整的导航解析流程

  1. 导航被触发。
  2. 在失活的组件里调用离开守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
  5. 在路由配置里调用 beforeEnter
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter
  8. 调用全局的 beforeResolve 守卫 (2.5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。

Vue-Router 内置了一个组件 <router-view>

Vue-Router 还内置了另一个组件 <router-link>

<router-link> 比起<a href="..."> 会好一些,理由如下:

无论是 HTML5 history 模式还是 hash 模式,它的表现行为一致,所以,当你要切换路由模式,或者在 IE9 降级使用 hash 模式,无须作任何变动。

在 HTML5 history 模式下,router-link 会守卫点击事件,让浏览器不再重新加载页面。

当你在 HTML5 history 模式下使用 base 选项之后,所有的 to 属性都不需要写(基路径)了。

路径变化是路由中最重要的功能,我们要记住以下内容:路由始终会维护当前的线路,路由切换的时候会把当前线路切换到目标线路,切换过程中会执行一系列的导航守卫钩子函数,会更改 url,同样也会渲染对应的组件,切换完毕后会把目标线路更新替换当前线路,这样就会作为下一次的路径切换的依据。

26、Vuex

Vuex 的初始化过程就分析完毕了,除了安装部分,我们重点分析了 Store 的实例化过程。我们要把 store 想象成一个数据仓库,初始化Vuex,就是在实例化Vuex,为了更方便的管理仓库,我们把一个大的 store 拆成一些 modules,整个 modules 是一个树型结构。每个 module 又分别定义了 stategettersmutationsactions,我们也通过递归遍历模块的方式都完成了它们的初始化。为了 module 具有更高的封装度和复用性,还定义了 namespace 的概念。最后我们还定义了一个内部的 Vue 实例,用来建立 state 到 getters 的联系,并且可以在严格模式下监测 state 的变化是不是来自外部,确保改变 state 的唯一途径就是显式地提交 mutation

Vuex 提供的一些常用 API 我们就分析完了,包括数据的存取、语法糖、模块的动态更新等。要理解 Vuex 提供这些 API 都是方便我们在对 store 做各种操作来完成各种能力,尤其是 mapXXX 的设计,让我们在使用 API 的时候更加方便,这也是我们今后在设计一些 JavaScript 库的时候,从 API 设计角度中应该学习的方向。

Vue源码的更多相关文章

  1. VUE 源码学习01 源码入口

    VUE[version:2.4.1] Vue项目做了不少,最近在学习设计模式与Vue源码,记录一下自己的脚印!共勉!注:此处源码学习方式为先了解其大模块,从宏观再去到微观学习,以免一开始就研究细节然后 ...

  2. Vue源码后记-其余内置指令(3)

    其实吧,写这些后记我才真正了解到vue源码的精髓,之前的跑源码跟闹着玩一样. go! 之前将AST转换成了render函数,跳出来后,由于仍是字符串,所以调用了makeFunction将其转换成了真正 ...

  3. Vue源码后记-钩子函数

    vue源码的马拉松跑完了,可以放松一下写点小东西,其实源码讲20节都讲不完,跳了好多地方. 本人技术有限,无法跟大神一样,模拟vue手把手搭建一个MVVM框架,然后再分析原理,只能以门外汉的姿态简单过 ...

  4. 大白话Vue源码系列(01):万事开头难

    阅读目录 Vue 的源码目录结构 预备知识 先捡软的捏 Angular 是 Google 亲儿子,React 是 Facebook 小正太,那咱为啥偏偏选择了 Vue 下手,一句话,Vue 是咱见过的 ...

  5. 大白话Vue源码系列(02):编译器初探

    阅读目录 编译器代码藏在哪 Vue.prototype.$mount 构建 AST 的一般过程 Vue 构建的 AST 题接上文,上回书说到,Vue 的编译器模块相对独立且简单,那咱们就从这块入手,先 ...

  6. 大白话Vue源码系列(03):生成AST

    阅读目录 AST 节点定义 标签的正则匹配 解析用到的工具方法 解析开始标签 解析结束标签 解析文本 解析整块 HTML 模板 未提及的细节 本篇探讨 Vue 根据 html 模板片段构建出 AST ...

  7. 大白话Vue源码系列(03):生成render函数

    阅读目录 优化 AST 生成 render 函数 小结 本来以为 Vue 的编译器模块比较好欺负,结果发现并没有那么简单.每一种语法指令都要考虑到,处理起来相当复杂.上篇已经生成了 AST,本篇依然对 ...

  8. 大白话Vue源码系列(04):生成render函数

    阅读目录 优化 AST 生成 render 函数 小结 本来以为 Vue 的编译器模块比较好欺负,结果发现并没有那么简单.每一种语法指令都要考虑到,处理起来相当复杂.上篇已经生成了 AST,本篇依然对 ...

  9. 大白话Vue源码系列(05):运行时鸟瞰图

    阅读目录 Vue 实例的生命周期 实例创建 响应的数据绑定 挂载到 DOM 节点 结论 研究 runtime 一边 Vue 一边源码 初看 Vue 是 Vue 源码是源码 再看 Vue 不是 Vue ...

  10. vue源码入口文件分析

    开发vue项目有段时间了, 之前用angularjs 后来用 reactjs 但是那时候一直没有时间把自己看源码的思考记录下来,现在我不想再浪费这 来之不易的思考, 我要坚持!! 看源码我个人感觉非常 ...

随机推荐

  1. C++11并发——多线程lock_gurad ,unique_lock (三)

    http://www.cnblogs.com/haippy/p/3346477.html struct defer_lock_t {}; 该类型的常量对象 defer_lock(defer_lock ...

  2. laravel Schema 查看索引是否存在

    Schema::connection('')->table($tableName, function (Blueprint $table) { $sm = Schema::getConnecti ...

  3. javascript 字符串的连接和截取

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  4. Codeforces 15 E. Triangles

    http://codeforces.com/problemset/problem/15/E 题意: 从H点走下去,再走回H点,不能走重复路径,且路径不能把黑色三角形包围的方案数 中间的黑色三角形把整张 ...

  5. PHP7 学习笔记(十)会话控制

    防守打法 1.设置Cookie,[基于内存的Cookie] setcookie('Username','tinywan'); setcookie('Age','24'); 2.查看Cookie存储位置 ...

  6. nmap扫描出现tcpwrapped

    FAQ tcpwrapped From SecWiki Jump to: navigation, search What does "tcpwrapped" mean? tcpwr ...

  7. bzoj 1724 优先队列 切割木板

    倒着的石子合并,注意不是取当前最长木板贪心做,而是取当前最小累加答案: 例如 4 5 6 7 若按第一种思路:ans=22+15+9 第二种:ans=22+13+9,可以先从中间某一块分开,这样答案更 ...

  8. C# 反射实例

    1.接口 using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ...

  9. JavaScript之事件绑定多个序列执行方法

    //一种事件绑定多个方法:以加载事件为例 function addEventLoad(func,isLog) { var oldOnLoad = window.onload; if (typeof w ...

  10. [C++]C++与C头文件辨析(比较)

    C++/C头文件辨析 C++标准库 C标准库 C++标准模板库 ios   vector iomanip   deque sstream   list fstream   map     set   ...