Vue0.11版本源码阅读系列一:实例化时做了什么

简介: Vue0.11版本源码阅读系列一:实例化时做了什么

vue3.0都已经出来很长一段时间了,而本系列将要带各位阅读的是0.11版本,也就是vue最早的正式版本,发布时间大概是六七年前,那时,嗯,太久远,都忘了我那时候在干什么,原因是2.0和3.0已经是一个很完善的框架了,代码量也很大,作为一个没啥源码阅读经验的老菜鸟,我不认为我有这个能力去看懂它,但同时又很想进一步的去看看它的真面目,思来想去,有两种思路,一是找到2.0或3.0的最早提交版本,然后一步一步的看它新增了什么,二是看它的早期版本,众所周知,早期版本一般都比较简单,最后决定先拿最早的版本练练手。


需要先说明的是0.11版本和2.x甚至是1.x语法区别都是很大的,但是核心思想是一致的,所以我们主要聚焦于响应式原理、模板编译等问题,具体的api不是咱们的重点,此外,这个版本因为实在太早了,所以没有虚拟节点,没有diff算法,想看这些的可以看看这位大神的系列文章:github.com/answershuto…和他的小册:


juejin.cn/book/684473…,话不多说,开始吧。


跑起来


0.11版本官方文档:011.vuejs.org/guide/index…,仓库分支:github.com/vuejs/vue/t…。


目录结构如下:


image.png


看起来是不是挺清晰挺简单的,第一件事是要能把它跑起来,便于打断点进行调试,但是构建工具用的是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就可以启动一个带热更新的服务了:


image.png


也可以直接克隆我的仓库github.com/wanglin2/vu…,已经配置好了并且翻译了英文注释。


构造函数


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.prototype
extend(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.toString
exports.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 key
if (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版本源码阅读系列三:指令编译。



相关文章
|
8天前
|
存储 关系型数据库 分布式数据库
PostgreSQL 18 发布,快来 PolarDB 尝鲜!
PostgreSQL 18 发布,PolarDB for PostgreSQL 全面兼容。新版本支持异步I/O、UUIDv7、虚拟生成列、逻辑复制增强及OAuth认证,显著提升性能与安全。PolarDB-PG 18 支持存算分离架构,融合海量弹性存储与极致计算性能,搭配丰富插件生态,为企业提供高效、稳定、灵活的云数据库解决方案,助力企业数字化转型如虎添翼!
|
7天前
|
存储 人工智能 Java
AI 超级智能体全栈项目阶段二:Prompt 优化技巧与学术分析 AI 应用开发实现上下文联系多轮对话
本文讲解 Prompt 基本概念与 10 个优化技巧,结合学术分析 AI 应用的需求分析、设计方案,介绍 Spring AI 中 ChatClient 及 Advisors 的使用。
349 130
AI 超级智能体全栈项目阶段二:Prompt 优化技巧与学术分析 AI 应用开发实现上下文联系多轮对话
|
19天前
|
弹性计算 关系型数据库 微服务
基于 Docker 与 Kubernetes(K3s)的微服务:阿里云生产环境扩容实践
在微服务架构中,如何实现“稳定扩容”与“成本可控”是企业面临的核心挑战。本文结合 Python FastAPI 微服务实战,详解如何基于阿里云基础设施,利用 Docker 封装服务、K3s 实现容器编排,构建生产级微服务架构。内容涵盖容器构建、集群部署、自动扩缩容、可观测性等关键环节,适配阿里云资源特性与服务生态,助力企业打造低成本、高可靠、易扩展的微服务解决方案。
1335 8
|
7天前
|
人工智能 Java API
AI 超级智能体全栈项目阶段一:AI大模型概述、选型、项目初始化以及基于阿里云灵积模型 Qwen-Plus实现模型接入四种方式(SDK/HTTP/SpringAI/langchain4j)
本文介绍AI大模型的核心概念、分类及开发者学习路径,重点讲解如何选择与接入大模型。项目基于Spring Boot,使用阿里云灵积模型(Qwen-Plus),对比SDK、HTTP、Spring AI和LangChain4j四种接入方式,助力开发者高效构建AI应用。
336 122
AI 超级智能体全栈项目阶段一:AI大模型概述、选型、项目初始化以及基于阿里云灵积模型 Qwen-Plus实现模型接入四种方式(SDK/HTTP/SpringAI/langchain4j)
|
6天前
|
监控 JavaScript Java
基于大模型技术的反欺诈知识问答系统
随着互联网与金融科技发展,网络欺诈频发,构建高效反欺诈平台成为迫切需求。本文基于Java、Vue.js、Spring Boot与MySQL技术,设计实现集欺诈识别、宣传教育、用户互动于一体的反欺诈系统,提升公众防范意识,助力企业合规与用户权益保护。
|
18天前
|
机器学习/深度学习 人工智能 前端开发
通义DeepResearch全面开源!同步分享可落地的高阶Agent构建方法论
通义研究团队开源发布通义 DeepResearch —— 首个在性能上可与 OpenAI DeepResearch 相媲美、并在多项权威基准测试中取得领先表现的全开源 Web Agent。
1424 87
|
6天前
|
JavaScript Java 大数据
基于JavaWeb的销售管理系统设计系统
本系统基于Java、MySQL、Spring Boot与Vue.js技术,构建高效、可扩展的销售管理平台,实现客户、订单、数据可视化等全流程自动化管理,提升企业运营效率与决策能力。
|
7天前
|
弹性计算 安全 数据安全/隐私保护
2025年阿里云域名备案流程(新手图文详细流程)
本文图文详解阿里云账号注册、服务器租赁、域名购买及备案全流程,涵盖企业实名认证、信息模板创建、域名备案提交与管局审核等关键步骤,助您快速完成网站上线前的准备工作。
265 82
2025年阿里云域名备案流程(新手图文详细流程)