vue核心 - 原理分析 (初版)

614 阅读19分钟

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.options上面有没有el属性,如果有的话使用vm.options 上面有没有 el 属性,如果有的话使用 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)添加的回调>按照先进先出的执行顺序去执行回调函数,在flushSchedulerQueue中去调用watcher.run()>vm.render进而更新视图,在手动使用this.nextTick(callBack) 添加的回调 -> 按照先进先出的执行顺序去执行回调函数,在flushSchedulerQueue中去调用 `watcher.run()` -> `vm._render` 进而更新视图,在手动使用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 / @孟思行 提供的优质文章

JavaScript 标准参考教程(alpha)

重新认识 package.json

package.json详解

Vue源码阅读

图解 Vue 响应式原理