0.前备知识
- Flow / TypeScript
- ES6语法
- JavaScript设计模式
- 函数式编程 / 高级函数
1.文件结构
文件结构在vue的CONTRIBUTING.md中有介绍,这边直接翻译过来:
├── scripts ------------------------------- 包含与构建相关的脚本和配置文件
│ ├── alias.js -------------------------- 源码中使用到的模块导入别名
│ ├── config.js ------------------------- 项目的构建配置
├── build --------------------------------- 构建相关的文件,一般情况下我们不需要动
├── dist ---------------------------------- 构建后文件的输出目录
├── examples ------------------------------ 存放一些使用Vue开发的应用案例
├── flow ---------------------------------- JS静态类型检查工具 [Flow](https://flowtype.org/) 的类型声明
├── package.json
├── test ---------------------------------- 测试文件
├── src ----------------------------------- 源码目录
│ ├── compiler -------------------------- 编译器代码,用来将 template 编译为 render 函数
│ │ ├── parser ------------------------ 存放将模板字符串转换成元素抽象语法树的代码
│ │ ├── codegen ----------------------- 存放从抽象语法树(AST)生成render函数的代码
│ │ ├── optimizer.js ------------------ 分析静态树,优化vdom渲染
│ ├── core ------------------------------ 存放通用的,平台无关的运行时代码
│ │ ├── observer ---------------------- 响应式实现,包含数据观测的核心代码
│ │ ├── vdom -------------------------- 虚拟DOM的 creation 和 patching 的代码
│ │ ├── instance ---------------------- Vue构造函数与原型相关代码
│ │ ├── global-api -------------------- 给Vue构造函数挂载全局方法(静态方法)或属性的代码
│ │ ├── components -------------------- 包含抽象出来的通用组件,目前只有keep-alive
│ ├── server ---------------------------- 服务端渲染(server-side rendering)的相关代码
│ ├── platforms ------------------------- 不同平台特有的相关代码
│ │ ├── weex -------------------------- weex平台支持
│ │ ├── web --------------------------- web平台支持
│ │ │ ├── entry-runtime.js ---------------- 运行时构建的入口
│ │ │ ├── entry-runtime-with-compiler.js -- 独立构建版本的入口
│ │ │ ├── entry-compiler.js --------------- vue-template-compiler 包的入口文件
│ │ │ ├── entry-server-renderer.js -------- vue-server-renderer 包的入口文件
│ ├── sfc ------------------------------- 包含单文件组件(.vue文件)的解析逻辑,用于vue-template-compiler包
│ ├── shared ---------------------------- 整个代码库通用的代码
几个重要的目录:
- compiler: 编译,用来将template转化为render函数
- core: Vue的核心代码,包括响应式实现、虚拟DOM、Vue实例方法的挂载、全局方法、抽象出来的通用组件等
- platform: 不同平台的入口文件,主要是 web 平台和 weex 平台的,不同平台有其特殊的构建过程,当然我们的重点是 web 平台
- server: 服务端渲染(SSR)的相关代码,SSR 主要把组件直接渲染为 HTML 并由 Server 端直接提供给 Client 端
- sfc: 主要是 .vue 文件解析的逻辑
- shared: 一些通用的工具方法,有一些是为了增加代码可读性而设置的
2. package.json文件
每个项目(npm上下载的包,或者其他的nodejs项目)的根目录下面,一般都有一个 package.json
文件,定义了这个项目所需要的各种模块,以及项目的配置信息(比如名称、版本、许可证、如何启动项目、运行脚步等元数据)。 npm install
命令根据这个配置文件,自动下载所需的模块。package.json文件就是一个JSON对象,该对象的每一个成员就是当前项目的一项设置。
package.json的作用
- 作为一个描述文件,描述了你的项目所依赖的包
- 允许我们使用 “语义化版本规则”(后面介绍)指明你项目依赖包的版本
- 让你的构建更好地与其他开发者分享,便于重复使用
package.json的创建
使用命令行工具客户端CLI或者按照规则手动创建
npm init
// 接下来按照启动命令行调查问卷来配置
...
// 按照默认配置创建
npm init -y
npm init --yes
package.json的配置
必须字段
1、name 定义了模块\包的名称,其命名时需要遵循官方的一些规范和建议:
- 模块名会成为模块 url、命令行中的一个参数或者一个文件夹名称,任何非 url 安全的字符在模块名中都不能使用(我们可以使用 validate-npm-package-name 包来检测模块名是否合法);
- 语义化模块名,可以帮助开发者更快的找到需要的模块,并且避免意外获取错误的模块;
- 若模块名称中存在一些符号,将符号去除后不得与现有的模块名重复,例如:由于 react-router-dom 已经存在,react.router.dom、reactrouterdom 都不可以再创建
- 长度必须小于等于214个字符,不能以"."(点)或者"_"(下划线)开头,不能包含大写字母- -name 字段不能与其他模块名重复
我们可以执行以下命令查看模块名是否已经被使用,如果模块存在,可以看到该模块的一些基本信息;如果该模块名从未被使用过,则会抛出 404 错误。
npm view <packageName>
2、version 当前包的版本号,初次建立默认为1.0.0
- npm 包中的模块版本都需要遵循 SemVer 规范,该规范的标准版本号采用 X.Y.Z 的格式,其中 X、Y 和 Z 均为非负的整数,且禁止在数字前方补零:
- X 是主版本号(major):修改了不兼容的 API
- Y 是次版本号(minor):新增了向下兼容的功能
- Z 为修订号(patch):修正了向下兼容的问题
- 当某个版本改动比较大、并非稳定而且可能无法满足预期的兼容性需求时,我们可能要先发布一个先行版本,先行版本号可以加到主版本号.次版本号.修订号的后面,通过 - 号连接一连串以句点分隔的标识符和版本编译信息:
- 内部版本(alpha)
- 公测版本(beta)
- 正式版本的候选版本rc(即 Release candiate)
我们可以执行以下命令查看模块的版本:
// 查看某个模块的最新版本
npm view <packageName> version
// 查看某个模块的所有历史版本
npm view <packageName> versions
可选字段
1、描述信息(description & keywords)、模块创建者与贡献者(author & contributors)
- description 字段用于添加模块的描述信息,便于用户了解该模块。
- keywords 字段用于给模块添加关键字。
- author 字段用于定义模块的创建者,通常是一个人
- contributors 字段用于定义对模块的贡献者,可以有多个人
- 当我们使用 npm 检索模块时,会对模块中的 description 字段和 keywords 字段进行匹配,写好 package.json中的 description 和 keywords 将有利于增加我们模块的曝光率
2、 安装项目依赖(dependencies & devDependencies)
- dependencies字段指定了项目运行所依赖的模块(生产环境使用),如 vue、 vux、 immer等插件库
- devDependencies 字段指定了项目开发所需要的模块(开发环境使用),如 webpack、typescript、babel等:
- 单元测试支撑(mocha、chai);
- 语法兼容(babel);
- 语法转换(jsx to js、coffeescript to js、typescript to js)
- 程序构建与优化(webpack、gulp、grunt、uglifyJS);
- css 处理器(postCSS、SCSS、Stylus);
- 代码规范(eslint);
"dependencies": {
"immer": "^7.0.7",
"vue": "^2.5.2",
"vue-router": "^3.0.1",
"vuex": "^3.3.0"
},
"devDependencies": {
"autoprefixer": "^7.1.2",
"babel-core": "^6.22.1",
"babel-eslint": "^8.2.1",
"babel-helper-vue-jsx-merge-props": "^2.0.3",
"babel-jest": "^21.0.2",
"babel-loader": "^7.1.1",
"babel-plugin-dynamic-import-node": "^1.2.0",
"babel-plugin-syntax-jsx": "^6.18.0",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-plugin-transform-vue-jsx": "^3.5.0",
"babel-preset-env": "^1.3.2",
"babel-preset-stage-2": "^6.22.0",
"babel-register": "^6.22.0",
"chalk": "^2.0.1",
"chromedriver": "^2.27.2",
"copy-webpack-plugin": "^4.0.1",
"cross-spawn": "^5.0.1",
"css": "^2.2.4",
"css-loader": "^0.28.11",
"eslint": "^4.15.0",
"eslint-config-standard": "^10.2.1",
"eslint-friendly-formatter": "^3.0.0",
"eslint-loader": "^1.7.1",
"eslint-plugin-import": "^2.7.0",
"eslint-plugin-node": "^5.2.0",
"eslint-plugin-promise": "^3.4.0",
"eslint-plugin-standard": "^3.0.1",
"eslint-plugin-vue": "^4.0.0",
"extract-text-webpack-plugin": "^3.0.0",
"file-loader": "^1.1.4",
"friendly-errors-webpack-plugin": "^1.6.1",
"html-webpack-plugin": "^2.30.1",
"jest": "^22.0.4",
"jest-serializer-vue": "^0.3.0",
"nightwatch": "^0.9.12",
"node-notifier": "^5.1.2",
"optimize-css-assets-webpack-plugin": "^3.2.0",
"ora": "^1.2.0",
"portfinder": "^1.0.13",
"postcss-import": "^11.0.0",
"postcss-loader": "^2.0.8",
"postcss-url": "^7.2.1",
"rimraf": "^2.6.0",
"sass": "^1.26.5",
"sass-loader": "^7.3.1",
"selenium-server": "^3.0.1",
"semver": "^5.3.0",
"shelljs": "^0.7.6",
"style": "^0.0.3",
"style-loader": "^1.2.1",
"stylus-loader": "^3.0.2",
"uglifyjs-webpack-plugin": "^1.1.1",
"url-loader": "^0.5.8",
"vue-jest": "^1.0.2",
"vue-loader": "^13.3.0",
"vue-style-loader": "^3.0.1",
"vue-template-compiler": "^2.5.2",
"webpack": "^3.12.0",
"webpack-bundle-analyzer": "^2.9.0",
"webpack-dev-server": "^2.9.1",
"webpack-merge": "^4.1.0"
}
有了 package.json 文件,开发直接使用 npm install / yarn install 命令,就会在当前目录中自动安装所需要的模块,安装完成项目所需的运行和开发环境就配置好了
npm install
// 安装指定模块
// 后边没有参数时,表示安装到dependencies属性,
// --save参数表示将该模块写入dependencies属性,
// --save-dev表示将该模块写入devDependencies属性
npm install express
npm install express --save
npm install express --save-dev
3、简化终端命令(scripts) scripts是一个由脚本命令组成的hash对象,他们在包不同的生命周期中被执行。key是生命周期事件,value是要运行的命令。 指定了运行脚本命令的npm命令行缩写,比如start指定了运行npm run start时,所要执行的命令。我们可以自定义我们想要的运行脚步命令
当我执行 npm run
的时候,就会自动新建一个 Shell,在这个 Shell 里面执行指定的脚本命令。因此,只要是 Shell(一般是 Bash)可以运行的命令,就可以写在 npm 脚本里面。会把当前目录下的node_modules/.bin也拷贝到当前的系统的path(只是临时拷贝,执行结束后,在将PATH变量恢复原样), 所以当前目录的node_modules/.bin子目录里面的所有脚本,都可以直接用脚本名调用,而不必加上路径
- 执行命令 echo xxx
- 执行node_modules/.bin 下的文件
"scripts": {
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
"start": "npm run dev",
"unit": "jest --config test/unit/jest.conf.js --coverage",
"e2e": "node test/e2e/runner.js",
"test": "npm run unit && npm run e2e",
"lint": "eslint --ext .js,.vue src test/unit test/e2e/specs",
"build": "node build/build.js"
}
4、定义项目入口(main) 指定了项目加载的入口文件,这个字段的默认值是模块根目录下面的index.js
"main": "src/index.js"
5、发布文件配置(files)
files 字段用于描述我们使用 npm publish 命令后推送到 npm 服务器的文件列表,如果指定文件夹,则文件夹内的所有内容都会包含进来。
我们可以查看下载的 antd 的 package.json 的files 字段,内容如下:
"files": [
"dist",
"lib",
"es"
]
可以看到下载后的 antd 包是下面的目录结构
6、定义私有模块(private) 一般公司的非开源项目,都会设置 private 属性的值为 true,这是因为 npm 拒绝发布私有模块,通过设置该字段可以防止私有模块被无意间发布出去。
"private": true
7、指定模块适用系统和 cpu 架构(os & cpu)
假如我们开发了一个模块,只能跑在 darwin 系统下,我们需要保证 windows 用户不会安装到该模块,从而避免发生不必要的错误。 这时候,使用 os 属性则可以帮助我们实现以上的需求,该属性可以指定模块适用系统的系统,或者指定不能安装的系统黑名单(当在系统黑名单中的系统中安装模块则会报错)
"os" : [ "darwin", "linux" ] # 适用系统
"os" : [ "!win32" ] # 黑名单
"cpu" : [ "x64", "ia32" ] # 适用 cpu
"cpu" : [ "!arm", "!mips" ] # 黑名单
8、指定项目 node 版本和 npm 版本(engines)
有时候,新拉一个项目的时候,由于和其他开发使用的 node 版本不同,导致会出现很多奇奇怪怪的问题(如某些依赖安装报错、依赖安装完项目跑步起来等),为了实现项目开箱即用的伟大理想,这时候可以使用 package.json 的 engines 字段来指定项目 node 版本,该字段也可以指定适用的 npm 版本
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
}
9、自定义命令(bin)
bin字段用来指定各个内部命令对应的可执行文件的位置
"bin": {
"someTool": "./bin/someTool.js"
}
上面代码指定,someTool 命令对应的可执行文件为 bin 子目录下的 someTool.js。Npm会寻找这个文件,在node_modules/.bin/目录下建立符号链接。在上面的例子中,someTool.js会建立符号链接node_modules/.bin/someTool。由于node_modules/.bin/目录会在运行时加入系统的PATH变量,因此在运行npm时,就可以不带路径,直接通过命令来调用这些脚本。
因此,像下面这样的写法可以采用简写:
scripts: {
start: './node_modules/bin/someTool.js build'
}
// 简写为
scripts: {
start: 'someTool build'
}
10、包代码存放(repository)
你可以通过提供 repository 字段来记录项目代码所在的资源库。该字段是一个对象,用于定义源代码所在的 url 及其使用的版本控制系统的类型。对于开源项目,可能是以 Git 作为版本控制系统的 GitHub 或 Bitbucket
"repository": {
"type": "git",
"url": "https://github.com/osiolabs/example.git"
}
11、设置应用根路径(homepage)
当我们使用 create-react-app 脚手架搭建的 React 项目,默认是使用内置的 webpack 配置,当package.json 中不配置 homepage 属性时,build 打包之后的文件资源应用路径默认是 /,如下图:
一般来说,我们打包的静态资源会部署在 CDN 上,为了让我们的应用知道去哪里加载资源,则需要我们设置一个根路径,这时可以通过 package.json 中的 homepage 字段设置应用的根路径。 当我们设置了 homepage 属性后:
"homepage": "https://xxxx.cdn/my-project",
打包后的资源路径就会加上 homepage 的地址:
3. vue运行机制
当我们在 main.js 里 new Vue(options)
后,Vue 会调用构造函数的 this._init(options)
方法,这个方法是在初始的时候就已经挂载在Vue构造函数的原型对象 Vue.prototype
上的
// src/core/instance/index.js
// Vue构造函数
function Vue (options) {
// 初始化
this._init(options)
}
// 这一系列 mixin 方法是往Vue.prototype上各种挂载,这是在加载的时候已经挂载好的
initMixin(Vue) // 给Vue.prototype添加:_init函数,...
stateMixin(Vue) // 给Vue.prototype添加:$data属性, $props属性, $set函数, $delete函数, $watch函数,...
eventsMixin(Vue) // 给Vue.prototype添加:$on函数, $once函数, $off函数, $emit函数, $watch方法,...
lifecycleMixin(Vue) // 给Vue.prototype添加: _update方法, $forceUpdate函数, $destroy函数,...
renderMixin(Vue) // 给Vue.prototype添加: $nextTick函数, _render函数,...
export default Vue
初始化方法解析 _init(options)
_init(options)
方法会对当前 vm 实例进行一系列初始化设置
// src/core/instance/index.js
Vue.prototype._init = function(options?: Object) {
const vm: Component = this
initLifecycle(vm) // 初始化生命周期 src/core/instance/lifecycle.js
initEvents(vm) // 初始化事件 src/core/instance/events.js
initRender(vm) // 初始化render src/core/instance/render.js
callHook(vm, 'beforeCreate') // 调用beforeCreate钩子
initInjections(vm) // 初始化注入值 before data/props src/core/instance/inject.js
initState(vm) // 挂载 data/props/methods/watcher/computed
initProvide(vm) // 初始化Provide after data/props
callHook(vm, 'created') // 调用created钩子
if (vm.$options.el) { // $options可以认为是我们传给 `new Vue(options)` 的options
vm.$mount(vm.$options.el) // $mount方法
}
}
- 我们的数据响应式化就是在此调用
initState(vm)
方法实现的,通过 Object.defineProperty() 方法对需要响应式化的对象设置 getter/setter,以此为基础进行依赖收集和派发更新,达到数据变化驱动视图变化的目的。 - 最后检测 vm.mount 方法挂载 vm,形成数据层和视图层的联系。这也是如果没有提供 el 选项就需要自己手动
vm.$mount('#app')
的原因。 - 我们看到 beforeCreate 钩子是在
initState(vm)
方法之前调用的,所以我们在 beforeCreate 钩子是无法对数据进行操作的,因为此时数据还没有挂载到vm实例上;而created 钩子是initState(vm)
方法之后、在挂载$mount
之前调用的,所以我们在 created 钩子触发之前是无法操作 DOM 的但是可以操作数据,这是因为此时还没有渲染到 DOM 上而数据已经挂载到vm实例上去了。
初始化数据
数据响应式化 initState(vm)
vue数据(props、methods、data、computed、watch)的响应式化就是 initState(vm)
来实现的
// src/core/instance/state.js
export function initState(vm: Component) {
const opts = vm.$options
if (opts.props) initProps(vm, opts.props) // 初始化props
if (opts.methods) initMethods(vm, opts.methods) // 初始化methods
if (opts.data) initData(vm) // 初始化data
if (opts.computed) initComputed(vm, opts.computed) // 初始化computed
if (opts.watch) initWatch(vm, opts.watch) // 初始化watch
}
1、initProps(vm, opts.props) 初始化props
待完成。。。
2、initMethods(vm, opts.props) 初始化methods
待完成。。。
**3、data数据的响应式化 initData(vm)
**
// src/core/instance/state.js
function initData(vm: Component) {
// 获取组件的 data 属性
let data = vm.$options.data
// 判断 data 是不是函数,是则取返回值,不是则取自身
data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}
// 给data做响应式处理
observe(data, true /* asRootData */)
}
// src/core/observer/index.js
export function observe (value: any, asRootData: ?boolean): Observer | void {
let ob: Observer | void
// 尝试给创建一个Observer实例 __ob__,如果成功创建则返回新的Observer实例,如果已有Observer实例则返回现有的Observer实例
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
return ob
}
在初始化data数据时,将传入的数据实例化为 Observer 对象,在实例化的时候递归遍历传入的数据对象,然后对数据对象的每一个属性做了两件事情
- 一是,给这个属性创建了一个依赖收集器 Dep
- 二是使用 Object.defineProperty(value, key, {}) 劫持了它的 getter 和 setter 这样当数据触发 get 查询时,会将当前的 Watcher 对象加入到依赖收集池 Dep 中,当数据 Data 变化时,会触发 set 通知所有使用到这个 Data 的 Watcher 对象去 update 视图
这里有三个重要的概念 Observe、Dep、Watcher
- Observe 类,递归遍历数据对象,给每一个对象属性创建一个依赖收集器Dep,然后使用Object.defineProperty劫持每一个属性的 getter/seeter 用于做依赖收集和派发更新
- Dep 类,在访问数据时收集依赖该数据的 Watcher ,在修改数据的时候通知所有的依赖 Watcher 更新视图
- Watcher 类,开启异步队列更新视图
Observe
在Observe类中,递归遍历传入的数据对象,获取对象的属性和值,为每一个属性创建一个依赖收集的容器Dep,使用闭包的形式将这个容器缓存起来,在访问这个属性时就会触发 get 方法,将当前的 Watcher 对象加入到依赖收集池 Dep 中,在修改这个属性时就会触发 set 方法,去通知所有依赖这个属性的 Watcher 更新对应的视图
// src/core/observer/index.js
export class Observer {
value: any;
dep: Dep;
constructor (value: any) {
value: any;
this.dep = new Dep()
def(value, '__ob__', this) // def方法保证不可枚举
if (Array.isArray(value)) { // 单独处理数组的响应式
if (hasProto) { // 判断浏览器是否支持__proto__
protoAugment(value, arrayMethods) // 支持 __proto__ 直接将重写的数组方法挂在 __proto__ 属性上
} else {
copyAugment(value, arrayMethods, arrayKeys) // 不支持 __proto__ 则将重写的数组方数组对象上
}
this.observeArray(value)
} else {
this.walk(value)
}
}
// 遍历对象的每一个属性并将它们转换为getter/setter
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) { // 把所有可遍历的对象响应式化
defineReactive(obj, keys[i])
}
}
// 遍历数组的每一个成员,将其实例化一个 Observer 对象实例
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean) {
const dep = new Dep() // 在每个响应式键值的闭包中定义一个dep对象
// 如果之前该对象已经预设了getter/setter则将其缓存,新定义的getter/setter中会将其执行
const getter = property && property.get
const setter = property && property.set
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val // 如果原本对象拥有getter方法则执行
if (Dep.target) { // 如果当前有watcher在读取当前值
dep.depend() // 那么进行依赖收集,dep.addSub
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val // 先getter
if (newVal === value || (newVal !== newVal && value !== value)) { // 如果跟原来值一样则不管
return
}
if (setter) { setter.call(obj, newVal) } // 如果原本对象拥有setter方法则执行
else { val = newVal }
dep.notify() // 如果发生变更,则通知更新,调用watcher.update()
}
})
}
数组的特殊处理
为了能够在使用数组方法时,也能够响应式的更新视图
// src/core/observer/array.js
// 创建一个实例对象,这个对象指向的原型对象与Array构造函数的原型对象一致
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
// 遍历需要重写的数组方法
// 使用Object.defineProperty方法将数据方法挂到arrayMethods上
// 当数组调用这些方法时其实调用的是改写之后的方法,里面包含了通知更新视图的代码
methodsToPatch.forEach(function (method) {
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
// 调用Array
const result = original.apply(this, args)
const ob = this.__ob__
// 使新增加的数据响应式化
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// 通知更新视图
ob.dep.notify()
return result
})
})
Dep
在Dep类中,收集依赖该数据的Watcher,并且当该数据发生变化时通知所有收集到的Watcher去更新视图
// src/core/observer/dep.js
export default class Dep {
static target: ?Watcher; // 当前是谁在进行依赖的收集
id: number; // Dep实例的id,为了方便去重
subs: Array<Watcher>; // subs 存放搜集到的 Watcher 对象集合
constructor () {
this.id = uid++
this.subs = []
}
// 添加一个观察者对象
addSub (sub: Watcher) {
this.subs.push(sub)
}
// 移除一个观察者对象
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
// 依赖收集,当存在Dep.target的时候把自己添加观察者的依赖中
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 通知所有订阅者
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
// 调用对应的 Watcher,更新视图
subs[i].update()
}
}
}
Watcher
在Watcher类中,实现了渲染方法 _render
和 Dep 的关联, 初始化 Watcher 的时候,打上 Dep.target 标识,然后调用 get 方法进行页面渲染
// src/core/observer/watcher.js
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = ''
// parse expression for getter
// 这里的 expOrFn 其实就是 vm._render
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
this.value = this.lazy ? undefined : this.get()
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
// 将当前 Watcher 推入 Watcher栈 中,并将当前 Watcher 赋予 Dep.target
pushTarget(this)
/*********************************
function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
*********************************/
// this.getter 其实就是 vm._render
// vm._render 用来生成虚拟 dom、执行 dom-diff、更新真实 dom
// 那么在 渲染 的时候,就会去访问数据,此时就会触发数据的 get 方法,而在上面已经将 Dep.target 标记为当前 Watcher 对象了
// 就能够将当前 Watcher 收集到该属性的依赖收集器 Dep 中
const vm = this.vm
let value = this.getter.call(vm, vm)
// 这个是用于watch Watcher
// 当在组件中使用了watch属性,并且使用了将deep设置为true,例如
// watch:{
// value:{
// handle(val){
// return val
// }
// deep:true
// }
// }
// 此时就需要
if (this.deep) {
traverse(value)
}
// 渲染之后就将该 Watcher 从 Watcher栈 中移除,并将 Dep.target 置为上一个 Watcher 对象,没有就置为undefined
popTarget()
/*********************************
function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
*********************************/
this.cleanupDeps()
return value
}
// 添加一个依赖关系到Deps集合中
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
// 清理newDeps里没有的无用watcher依赖
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
// 更新视图
update () {
if (this.lazy) {
// 用于computed Watcher
this.dirty = true
} else {
// 开启异步队列,批量更新 Watcher
queueWatcher(this)
}
}
// 调用 get 方法最终,执行 vm._render 更新视图
run () {
if (this.active) {
const value = this.get()
}
}
// 用于computed Watcher
evaluate () {
this.value = this.get()
this.dirty = false
}
// 收集该watcher的所有deps依赖
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
// 将自身从所有依赖收集订阅列表删除
teardown () {
if (this.active) {
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
}
Watcher 有下面几种使用场景:
- render Watcher 渲染 Watcher,渲染视图用的 watcher
- computed Watcher 计算属性 Watcher,因为计算属性即依赖别人也被人依赖,因此也会持有一个 Dep 实例
- watch Watcher 侦听器 Watcher
待完成。。。
总结一下,vue对于data的处理就是:
vue在初始化 Data 数据的时候会创建一个 Observer 类,在这个 Observer 中递归的去遍历 Data 数据的每一个属性,分别为每一个属性创建一个依赖收集器 Dep ,用来做依赖收集和派发更新,然后通过 Object.defineProperty 来完成 Data 数据所有属性的代理,当数据触发 get 查询时,会将当前的 Watcher 对象加入到依赖收集池 Dep 中,当数据 Data 变化时,会触发 set 通知所有使用到这个 Data 的 Watcher 对象去更新视图
渲染视图
-
_init(options)
方法中,数据初始化完毕之后,会调用vm.$mount(vm.$options.el)
方法, 将 Vue 实例渲染成 dom -
在数据更新的时候会调用 Watcher 类的
update
方法,开启异步队列更新视图
// src/core/observer/scheduler.js
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
// 在nextTick中会判断使用哪种异步的方法来调用回调函数
// flushSchedulerQueue 中会调用 watcher.run() 方法去更新视图
nextTick(flushSchedulerQueue)
}
}
}
所以借此我们可以来总结一下vue的批量异步更新与nextTick原理:
数据变化 -> 触发 set 方法 -> 调用 dep.notify() 方法,通知所有依赖的Watcher,调用对应的 update() 方法 -> 调用 nextTick() 方法,选择兼容的异步方法,按照顺序将回调函数推入异步队列中,这个回调函数包括 flushSchedulerQueue 和 通过 this.nextTick() 添加的callBack中,去执行自己的逻辑
this.$nextTick() 所做的其实就是往异步队列中添加任务
<div id="app">
<span id='name' ref='name'>{{ name }}</span>
<button @click='change'>change name</button>
<div id='content'></div>
</div>
<script>
new Vue({
el: '#app',
data() {
return {
name: 'SHERlocked93'
}
},
methods: {
change() {
const $name = this.$refs.name
this.$nextTick(() => console.log('setter前:' + $name.innerHTML))
this.name = ' name改喽 '
console.log('同步方式:' + this.$refs.name.innerHTML)
setTimeout(() => this.console("setTimeout方式:" + this.$refs.name.innerHTML))
this.$nextTick(() => console.log('setter后:' + $name.innerHTML))
this.$nextTick().then(() => console.log('Promise方式:' + $name.innerHTML))
}
}
})
</script>
// src/platforms/web/runtime/index.js
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
// src/platforms/web/util/index.js
function query (el: string | Element): Element {
if (typeof el === 'string') {
const selected = document.querySelector(el)
if (!selected) {
return document.createElement('div')
}
return selected
} else {
return el
}
}
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
}
callHook(vm, 'beforeMount')
let updateComponent = () => {
// 调用 vm._render 生成虚拟 dom
// 调用 vm._update(vnode) 渲染虚拟 dom
vm._update(vm._render(), hydrating)
}
//
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
生成虚拟 dom vm._render()
待完成。。。
渲染虚拟 dom vm._update()
待完成。。。
感谢 @前端小黑 / @草履虫的思考 / @SHERlocked93 / @孟思行 提供的优质文章