各位,你们没有看错,当初是2021年,vue3.0都曾经进去很长一段时间了,而本系列将要带各位浏览的是0.11版本,也就是vue最早的正式版本,公布工夫大略是六七年前,那时,嗯,太长远,都忘了我那时候在干什么,起因是2.0和3.0曾经是一个很欠缺的框架了,代码量也很大,作为一个没啥源码浏览教训的老菜鸟,我不认为我有这个能力去看懂它,但同时又很想进一步的去看看它的真面目,思来想去,有两种思路,一是找到2.0或3.0的最早提交版本,而后一步一步的看它新增了什么,二是看它的晚期版本,家喻户晓,晚期版本个别都比较简单,最初决定先拿最早的版本练练手。

须要先阐明的是0.11版本和2.x甚至是1.x语法区别都是很大的,然而核心思想是统一的,所以咱们次要聚焦于响应式原理、模板编译等问题,具体的api不是咱们的重点,此外,这个版本因为切实太早了,所以没有虚构节点,没有diff算法,想看这些的能够看看这位大神的系列文章:https://github.com/answershuto/learnVue和他的小册:https://juejin.cn/book/6844733705089449991,话不多说,开始吧。

跑起来

0.11版本官网文档:https://011.vuejs.org/guide/index.html,仓库分支:https://github.com/vuejs/vue/tree/0.11。

目录构造如下:

看起来是不是挺清晰挺简略的,第一件事是要能把它跑起来,便于打断点进行调试,然而构建工具用的是grunt,不会,所以简略的应用webpack来配置一下:

1.装置:npm install webpack webpack-cli webpack-dev-server html-webpack-plugin clean-webpack-plugin --save-dev,留神要去看看package.json外面是不是曾经有webpack了,有的话记得删了,不然版本不对。

2.在/src目录下新建一个index.js文件,用来作为咱们的测试文件,输出:

import Vue from './vue'new Vue({    el: '#app',    data: {        message: 'Hello Vue.js!'    }})

3.在package.json文件同级目录下新建一个index.html,输出:

<!DOCTYPE html><html><head>    <meta charset="utf-8" />    <title>demo</title></head><body>    <div id="app">        <p>{{message}}</p>        <input v-model="message">    </div></body></html>

4.在package.json文件同级目录下新建一个webpack配置文件webpack.config.js,输出:

const path = require('path');const HtmlWebpackPlugin = require('html-webpack-plugin');const { CleanWebpackPlugin } = require('clean-webpack-plugin');module.exports = {  mode: 'development',  entry: {    index: './src/index.js'  },  devtool: 'inline-source-map',  output: {    filename: '[name].bundle.js',    path: path.resolve(__dirname, 'dist'),  },  devServer: {    contentBase: './dist',    hot: true  },  plugins: [    new CleanWebpackPlugin({ cleanStaleWebpackAssets: false }),    new HtmlWebpackPlugin({      template: 'index.html'    }),  ],};

5.最初配置一下package.json的执行命令:

{    "scripts": {        "start": "webpack serve --hot only --host 0.0.0.0    },}

这样在命令行输出npm start就能够启动一个带热更新的服务了:

也能够间接克隆我的仓库https://github.com/wanglin2/vue_v0.11_analysis,曾经配置好了并且翻译了英文正文。

构造函数

Vue的初始化工作次要是给Vue的构造函数和原型挂载办法和属性。

增加静态方法:

function Vue (options) {  this._init(options)}extend(Vue, require('./api/global'))

增加动态属性:

Vue.options = {  directives  : require('./directives'),  filters     : require('./filters'),  partials    : {},  transitions : {},  components  : {}}

增加原型办法:

var p = Vue.prototypeextend(p, require('./instance/init'))extend(p, require('./instance/events'))extend(p, require('./instance/scope'))extend(p, require('./instance/compile'))extend(p, require('./api/data'))extend(p, require('./api/dom'))extend(p, require('./api/events'))extend(p, require('./api/child'))extend(p, require('./api/lifecycle'))

extend办法很简略,就是一个浅拷贝函数:

exports.extend = function (to, from) {  for (var key in from) {    to[key] = from[key]  }  return to}

实例代理data属性:

Object.defineProperty(p, '$data', {  get: function () {    return this._data  },  set: function (newData) {    this._setData(newData)  }})

_data就是创立vue实例时传入的data数据对象。

构造函数里只调用了_init办法,这个办法首先定义了一堆后续须要应用的属性,包含公开的和公有的,而后会进行选项合并、初始化数据察看、初始化事件和生命周期,这之后就会调用created生命周期办法,如果传递了$el属性,接下来就会开始编译。

选项合并

options = this.$options = mergeOptions(    this.constructor.options,    options,    this)

constructor.options就是上一节提到的那些动态属性,接下来看mergeOptions办法:

guardComponents(child.components)

首先调用了guardComponents办法,这个办法用来解决咱们传入的components选项,这个属性是用来注册组件的,比方:

new Vue({    components: {        'to-do-list': {            //...        }    }})

组件其实也是个vue实例,所以这个办法就是用来把它转换成vue实例:

function guardComponents (components) {  if (components) {    var def    for (var key in components) {      def = components[key]      if (_.isPlainObject(def)) {        def.name = key        components[key] = _.Vue.extend(def)      }    }  }}

isPlainObject办法用来判断是不是纯正的原始的对象类型:

var toString = Object.prototype.toStringexports.isPlainObject = function (obj) {  return toString.call(obj) === '[object Object]'}

vue创立可复用组件调用的是静态方法extend,用来创立Vue构造函数的子类,为啥不间接new Vue呢?extend做了啥非凡操作呢?不要走开,接下来更精彩。

其实extend如字面意思继承,其实返回的也是个构造函数,因为咱们晓得组件是可复用的,如果间接new一个实例,那么即便在多处应用这个组件,实际上都是同一个,数据什么的都是同一份,批改一个影响所有,显然是不行的。

如果不应用继承的话,就相当于每应用一次该组件,就须要应用该组件选项去实例化一个新的vue实例,貌似也能够,所以给每个组件都创立一个构造函数可能是不便扩大和调试吧。

exports.extend = function (extendOptions) {  extendOptions = extendOptions || {}  var Super = this  // 创立子类构造函数  var Sub = createClass(    extendOptions.name ||    Super.options.name ||    'VueComponent'  )  Sub.prototype = Object.create(Super.prototype)  Sub.prototype.constructor = Sub  Sub.cid = cid++  // 这里也调用了mergeOptions办法  Sub.options = mergeOptions(    Super.options,    extendOptions  )  Sub['super'] = Super  Sub.extend = Super.extend  // 增加静态方法,如:directive、filter、transition等注册办法,以及component办法  createAssetRegisters(Sub)  return Sub}

能够看到这个办法其实就是个类继承办法,个别咱们创立子类会间接定义一个办法来当做子类的构造函数,如:

function Par(name){    this.name = name}Par.prototype.speak = function (){    console.log('我叫' + this.name)}function Child(name){    Par.call(this, name)}Child.prototype = new Par()

然而Vue这里应用的是new Function的形式:

function createClass (name) {  return new Function(    'return function ' + _.classify(name) +    ' (options) { this._init(options) }'  )()}

正文里的解释是:This gives us much nicer output when logging instances in the console.粗心是不便在控制台打印。

回到选项合并办法:

var keyif (child.mixins) {    for (var i = 0, l = child.mixins.length; i < l; i++) {        parent = mergeOptions(parent, child.mixins[i], vm)    }}

因为每个mixins都可蕴含全副的选项,所以须要递归合并。

for (key in parent) {    merge(key)}for (key in child) {    if (!(parent.hasOwnProperty(key))) {        merge(key)    }}function merge (key) {    var strat = strats[key] || defaultStrat    options[key] = strat(parent[key], child[key], vm, key)}return options

而后是合并具体的属性,对不同的属性vue调用了不同的合并策略办法,有趣味的可自行浏览。

初始化数据察看

选项参数合并完后紧接着调用了_initScope办法:

exports._initScope = function () {  this._initData()  this._initComputed()  this._initMethods()  this._initMeta()}

该办法又调用了四个办法,一一来看。

_initData办法及后续请移步第二篇:vue0.11版本源码浏览系列二:数据察看。

_initComputed用来初始化计算属性:

function noop () {}exports._initComputed = function () {  var computed = this.$options.computed  if (computed) {    for (var key in computed) {      var userDef = computed[key]      var def = {        enumerable: true,        configurable: true      }      if (typeof userDef === 'function') {        def.get = _.bind(userDef, this)        def.set = noop      } else {        def.get = userDef.get          ? _.bind(userDef.get, this)          : noop        def.set = userDef.set          ? _.bind(userDef.set, this)          : noop      }      Object.defineProperty(this, key, def)    }  }}

设置计算属性的getttersetter,而后定义到实例上成为实例的一个属性,咱们都晓得计算属性所依赖的数据变动了它也会跟着变动,根据上述代码,仿佛不太显著,然而很容易了解的一点是通过this.xxx在任何时候援用计算属性它是会执行对应的函数的,所以拿到的值必定是最新的,问题就是应用了计算属性的模板如何晓得要更新,目前看不出来,后续再说。

bind办法用来设置函数的上下文对象,个别有:callapplybind三种办法,第三种办法执行后会返回一个新函数,这里vue应用apply简略模仿了一下bind办法,起因是比原生更快,毛病是不如原生欠缺:

exports.bind = function (fn, ctx) {  return function () {    return fn.apply(ctx, arguments)  }}

_initMethods就比较简单了,把办法都代理到this上,更方便使用:

exports._initMethods = function () {  var methods = this.$options.methods  if (methods) {    for (var key in methods) {      this[key] = _.bind(methods[key], this)    }  }}

上述办法都应用bind办法把函数的上下文设置为vue实例,这样能力在函数里拜访到实例上的其余办法或属性,这就是为什么不能应用箭头函数的起因,因为箭头函数没有本人的this

初始化事件

_initEvents办法会遍历watch选项并调用$watch办法来察看数据,所以间接看$watch办法:

exports.$watch = function (exp, cb, deep, immediate) {  var vm = this  var key = deep ? exp + '**deep**' : exp  var watcher = vm._userWatchers[key]  var wrappedCb = function (val, oldVal) {    cb.call(vm, val, oldVal)  }  if (!watcher) {    watcher = vm._userWatchers[key] =      new Watcher(vm, exp, wrappedCb, {        deep: deep,        user: true      })  } else {    watcher.addCb(wrappedCb)  }  if (immediate) {    wrappedCb(watcher.value)  }  return function unwatchFn () {    watcher.removeCb(wrappedCb)    if (!watcher.active) {      vm._userWatchers[key] = null    }  }}

查看要察看的表达式是否曾经存在,存在则追加该回调函数,否则创立并存储一个新的watcher实例,最初返回一个办法用来解除察看,所以要想了解最终的原理,还是得后续再看Watcher的实现。

这一步完结后就会触发created生命周期办法:this._callHook('created')

exports._callHook = function (hook) {  var handlers = this.$options[hook]  if (handlers) {    for (var i = 0, j = handlers.length; i < j; i++) {      handlers[i].call(this)    }  }  this.$emit('hook:' + hook)}

最初如果传了挂载元素,则会立刻开始编译,编译相干请浏览:vue0.11版本源码浏览系列三:指令编译。