共计 7065 个字符,预计需要花费 18 分钟才能阅读完成。
各位,你们没有看错,当初是 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.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)
}
}
}
设置计算属性的 gettter
和setter
,而后定义到实例上成为实例的一个属性,咱们都晓得计算属性所依赖的数据变动了它也会跟着变动,根据上述代码,仿佛不太显著,然而很容易了解的一点是通过 this.xxx
在任何时候援用计算属性它是会执行对应的函数的,所以拿到的值必定是最新的,问题就是应用了计算属性的模板如何晓得要更新,目前看不出来,后续再说。
bind
办法用来设置函数的上下文对象,个别有:call
、apply
、bind
三种办法,第三种办法执行后会返回一个新函数,这里 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 版本源码浏览系列三:指令编译。