Vue 源码初探

Vue 源码初探

流程图

初始化Vue构造函数.png

环境搭建

mkdir vue
cd vue 
yarn init
复制代码

安装babel

  1. 运行以下命令安装所需的包(package):
yarn add @babel/core @babel/cli @babel/preset-env -D
复制代码

2.在项目的根目录下创建一个命名为 babel.config.json 的配置文件(需要 v7.8.0 或更高版本),并将以下内容复制到此文件中:

{
  "presets": [
    [
      "@babel/env",
      {
        "targets": {
          "edge": "17",
          "firefox": "60",
          "chrome": "67",
          "safari": "11.1"
        },
        "useBuiltIns": "usage",
        "corejs": "3.6.5"
      }
    ]
  ]
}
复制代码

上述浏览器列表仅用于示例。请根据你所需要支持的浏览器进行调整。参见 此处 以了解 @babel/preset-env 可接受哪些参数。

上面内容来自于babel官网

  1. 运行此命令将 src 目录下的所有代码编译到 lib 目录:
./node_modules/.bin/babel src --out-dir lib
复制代码

你可以利用 npm@5.2.0 所自带的 npm 包运行器将 ./node_modules/.bin/babel 命令缩短为 npx babel

现在我们执行以下操作

cd src 
touch index.js
复制代码

//在index.js

function Vue() {
  console.log('hiiiihi')
}

export default Vue
复制代码

执行

npx babel src --out-dir lib
Successfully compiled 1 file with Babel (493ms).
复制代码

结果如下

1621735316535.png

rollup 搭建

yarn add rollup -D
复制代码

添加配置文件rollup.config.js

import { babel } from '@rollup/plugin-babel';

export default {
  input: 'src/index.js',
  output: {
    file: 'dist/vue.js',
    //IIFE  自执行函数,   UMD = AMD + CMD
    format: 'umd',
    name: 'Vue', //umd模块需要配置name,会将导出的模式放到window上
    sourcemap: true
  },
  plugins: [
    babel({ 
      exclude: 'node_modules/**' //去掉node_modules下面的所有文件夹,不进行编译
    })
  ]
}
复制代码

Vue 的初始化流程

下面我们正式开始编写,我们打开Vue.js的官网,把里面的内容抄过来,如下代码

<body>
  <div id="app">
    {{ message }}
  </div>
  <script src="./vue.js"></script>
  <!--<script src="https://cdn.jsdelivr.net/npm/vue"></script>-->
  <script>
    var app = new Vue({
      el: '#app',
      data: {
        message: 'Hello Vue!'
      }
    })
  </script>
</body>
复制代码

接着我们把引入cdn的vue类库换成我们自己编写的代码。

//vue.js

function Vue(options) {
  console.log(options)
  // this._init(options)
}
Vue.prototype._init = function(){
  console.log('初始化Vue 流程入口')
}
export default Vue
复制代码

1621735316535.png

现在就以上代码,我们用到的js的 构造函数 原型 (prototype) new。关于这些知识,在开始之前,我们先简单的瞄一眼 ^_*

  1. prototype 是构造函数的一个属性。
  2. 可以通过new 构造函数创建实例对象。
  3. 每个实例对象都可以调用构造函数上面prototype的方法或者属性。
//就上面代码来说。通过new Vue()  可以创建 app实例对象,
//在new Vue() 的时候可以通过this._init() 调用实例构造函数上面的方法
复制代码

new 的过程

我们先来讲讲new Vue()的过程

  1. 在内存中创建一个新对象。
  2. 这个新对象内部的[[Prototype]]特性被赋值为构造函数的 prototype 属性
  3. 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)
  4. 执行构造函数内部的代码(给新对象添加属性)
  5. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象
function Fn(name, age) {
  this.name = name
  this.age = age
}

Fn.prototype.xxx = function(){}

function myNew(Obj) {
  //创建一个新的对象
  var a = {}
  var arg = Array.from(arguments)
  //取出第一个参数, 就是要实例化额对象的构造函数
  var constructor = arg.shift()
  //构造函数上面会有一个 prototype的属性存放着这类对象的共有属性和方法
  //这句话相当于 a.__proto__ = constructor.prototype
  Object.setPrototypeOf(a, constructor.prototype)
  // 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)
  var result = constructor.apply(a, arg)
  // 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象
  return typeof result === 'object' ? result : a
}


var  t =  myNew(Fn, '天明', 16)
console.log(t)
复制代码

查看控制台输出

1621785075184.png

好了,看到这里我们再,继续写下面的代码。嗨,既然已经写到这块了,不妨把下面这段代码的内存图也一并奉上叭

var app = new Vue() 
复制代码

1621866581371.png

推荐阅读,大家可以点击去看一看,并亲手实验一下里面的代码,相信会有不少的收获,比如笔者在看着几篇文章的同时,可能还会不经意的看到其他的一些问题、比如

1. Function.__proto__ === Function.prototype    // true

2. Math 没有prototype 属性
复制代码

JS 的 new 到底是干什么的?

JS 中 proto 和 prototype 存在的意义是什么?

什么是 JS 原型链?

en .看了以上的东西,我们来做一道题吧。

写一下Foo和obj的原型链向上查找直到结束的整个过程?当给x赋值后,分别值为多少?

function Foo() {}; 
let obj = new Foo();
// obj 原型链
obj.__proto__ === Foo.prototype
Foo.prototype.__proto__ ===  Object.prototype
Object.prototype.__proto__ === null
// Foo 原型链
Foo.__proto__ === Function.prototype
Function.prototype.__proto__ === Object.prototype
Object.prototype.__proto__ === null

Function.prototype.x = 100;
Foo.x; // 100
obj.x; // undefined

//本题考查 实例的原型链和构造函数的原型链
//说明了构造函数的原型链和实例的原型链的开端是不一样的,虽然最后的结局是一样的。
复制代码

好。我们继续上面的代码,为了分离代码,我们开始这样构建代码, 我们提取一个单独的文件将Vue当做函数的参数传递到函数内部,在函数内部给其原型上面扩展方法。

//init.js

export function initMixin (Vue) {
  Vue.prototype._init = function (options) {
    console.log('初始化Vue 流程入口')
    console.log(options)
  }
}
复制代码

//index.js

import { initMixin } from "./init"

function Vue(options) {
  this._init(options)
}
initMixin(Vue)

export default Vue
复制代码

Vue的对象劫持

上面我们写到给Vue原型添加_init方法,下面我们开始初始化Vue的状态 initState() 方法

import { initState } from "./state"

export function initMixin (Vue) {
  Vue.prototype._init = function (options) {
    //这里的 this 其实就是外面new 出来的实例
    const vm = this

    //把用户的选项放到vm 上,这样在其他的方法中都可以获取到了

    vm.$options = options  //为了后续方法,都可以获取到$options选项

    //options中包含了很多的选项  el data props 

    initState(vm)
  }
}
复制代码

//state.js

我们把当前实例vm 传递给initState() 从中就可以获取到用户传递的配置信息,想想大家开始写Vue的时候,在new Vue() 这个构造函数的时候传入的这个对象,我们现在要做的就是,拿到这个对象里面的不同的key 比如 data 、props 、computed、methods、等等,对这些参数做一些处理,让其在某些时刻发生作用。下面我们开始处理data属性。

import { observe } from "./observe"
import { isFunction } from "./utils"

export function initState(vm) {
  const opts = vm.$options

  //如果用户传递了data属性
  if(opts.data) {
    initData(vm)
  }
}


function initData(vm) {
  let data = vm.$options.data
  //检测,用户传递的data的类型   如果是函数的话,需要取函数的返回值当做data  
  //用call 是为了获取vm 上面data 
  data = isFunction(data) ? data.call(vm) : data

  //数据合法,开始监测
  //需要将data 变成响应式的,用Object.defineProperty, 重写data中的所有属性

  observe(data)
}
复制代码

现在我们已经确保data是一个对象了,{} 或者 [] ,现在我们姑且只认为data的类型只能是{},我们开始把data传入observe() 中让他具有响应式。所谓的响应式,目前我们先理解成,当我们写在data下面的属性在被获取、或者被设置的时候,可以告诉我们。用到的API,就是Object.defineProperty(),更多关于这个的讲解大家可以移步

MDN Object.defineProperty()

import { isObject } from "../utils";

//观察者对象
class Observer {
  constructor(data) {
    this.walk(data)
  }

  walk(data) {
    Object.keys(data).forEach((key)=>{
      let value = data[key]
      defineReactive(data, key, value)
    })
  }
}

/**
 * 
 * @param {当前对象} data 
 * @param {要监测的key} key 
 * @param {当前对象key的value, 被监测之前的值} value 
 */
function defineReactive(data, key, value){
  //如果当前对象的key 再是一个对象就进行深层监测
  observe(value)
  Object.defineProperty(data, key,{
    get() {
      return value
    },
    set(newValue) {
      //如果新值和老值相等 就不做处理
      if(value === newValue) return
      value = newValue
    }
  })
}


export function observe(data){
  //判断data 必须是一个对象
  if(!isObject(data)) {
    return
  }

  //开始观测 ,这里目前return 或者不return 都可以
  return new Observer(data)
}
复制代码

我们在使用的地方打印当前new 出来的实例, 可以看到他们的每个属性都拥有了一个get 和set方法。

1622259247273.png

性能优化

写到这块,我们是不是可以想一想一些关于性能优化的一些技巧。

  1. 不要把所有的数据都写在data里面,比如一些不会对ui页面产生影响或者说是不用进行监测的数据。
  2. 不要写数据的时候层次过深,尽量扁平化数据。
  3. 不要频繁的获取数据,会触发多次get方法,造成一定的性能损耗。
  4. 如果数据不需要响应式,可以使用Object.freeze进行属性的冻结。

MDN Object.freeze

这里有可能你会看见几个名词、可枚举、可配置、可修改、后两者很好理解其作用和用途、但是前者、不知道大家看到这个名词的时候,有没有想过问什么或有这个属性的出现呢?他有什么作用呢?如果想知道请观众老爷移步

for in 和 for of 的区别?

数组的变化侦测

上面我们用defineReactive 对data下面的所有对象属性进行了观测,接下来我们给data里面写一个数组看看会发生什么?

var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!',
    arr: [1, 2, 3]
  }
})

console.log(app)
复制代码

1622303845079.png

可以看到数组的每一项都被进行了观测,我们写下以下代码,进行测试

//尝试调用app上面的$options 里面的data.arr[0]
console.log(app.$options.data.arr[0])

//同时我们在defineReactive get()里面打印一句话

function defineReactive(data, key, value){
  //如果当前对象的key 再是一个对象就进行深层监测
  observe(value)
  Object.defineProperty(data, key,{
    get() {
      console.log('call get')
      return value
    },
    set(newValue) {
      //如果新值和老值相等 就不做处理
      if(value === newValue) return
      value = newValue
    }
  })
}
复制代码

我们可以看到,在我们读取属性的时候会进入get方法。

1622304678276.png

至于为什么Vue会选择重写数组方法,大家先没有必要较真,我在这边直接写我获取到的答案了,具体的大家有兴趣可以进入链接查看详细内容

主要有两点原因吧:

  1. Vue 的响应式是通过 Object.defineProperty() 实现的,这个 api 不能监听数组长度的变化,也就不能监听数组的新增。
  2. Vue 无法检测通过数组索引改变数组的操作,这不是 Object.defineProperty() api 的原因,而是 尤大认为性能消耗与带来的用户体验不成正比 ,对数组进行响应式检测会带来很大的性能消耗,因为数组项可能会大,比如1000条、10000条。

好,我们继续,开始数组的原数组的方法。

import { isArray, isObject } from "../utils";
import { arrayMethods } from "./array";

class Observer {
  constructor(data) {
    if(isArray(data)) {
      //如果当前dada的属性的类型是数组,就重写当前数组的原型方法
      data.__proto__ = arrayMethods
    }else {
      this.walk(data)
    }
  }
}
复制代码
array.js
//获取数组老的原型对象
let oldArrayProtoMethods = Array.prototype

//用数组原始的原型对象,创建一个新的对象
//作用就是 arrayMethods.__proto__ === oldArrayProtoMethods
export let arrayMethods = Object.create(oldArrayProtoMethods)

//把数组的可以改变原始数组的方法进行重写
let methods = [
  'push',
  'pop',
  'splice',
  'sort',
  'shift',
  'unshift',
  'reverse'
]


methods.forEach((method)=>{
  //给当前对象添加上要被重写的属性方法,对象查找属性是先从自身查找,然后再到原型链上去查找
  //这样既可以保证改变原数组的方法被重写,又可以在原型上面查找到 concat、slice。等方法
  //arrayMethods.push = function(){}
  //arrayMethods.pop = function(){}
  //arrayMethods.splice = function(){}
  //...
  arrayMethods[method] = function(){
    console.log('调用了数组被重写的方法')
  }
})

复制代码

测试

1622305640758.png

1622305666857.png

这样我们就又换了一种方式实现了,data里面数组的监听,是只能通过数组的这7个api调用的形式才可以触发监听。

Vue 实例取值的代理

//一开始vm实例和用户传递的data是没有关系的,为了外面的vm实例能获取到data 就给vm添加一个属性data,这样就可以用 vm._data进行数据的访问了,但是Vue的作者希望可用直接通过vm.message访问到传递给构造函数的参数里面的data的属性,我们就要再做一层代理
复制代码

Vue响应式原理剖析

Vue 的实现流程

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享