# Vue源码理解

# MVVM的三要素

  • 响应式:vue如何监听到data的每个属性变化

  • 模板引擎:vue的模板如何被解析,指令如何处理?

  • 渲染:vue的模板如何被渲染html?以及渲染过程

# vue中如何实现响应式?

  • 什么是响应式

    1. 修改data属性后,vue立刻监听到

    2. data属性被代理到vm上

  • Object.defineProperty

var obj = {}
var _name = 'evan'

Object.defineProperty(obj, 'name', {
  get: function() {
    console.log('get', _name)
    return _name
  },
  set: function (newVal) {
    console.log('set', newVal)
    _name = newVal
  }
})

console.log(obj._name) // 可以监听到
obj._name = 'wang'     // 可以监听到

所以,vue中数据改变,视图就改变,其核心就是通过Object.defineProperty实时监听到数据的变化

  • 模拟
var vm = {}
var data = {
  name: 'evan',
  age: 25
}

var key,value
for (key in data) {
  (function (key) {
    Object.defineProperty(vm, key, {
      get: function () {
        console.log('get', data[key]) // 监听
        return data[key]
      },
      set: function (newVal) {
        console.log('set', newVal) // 监听
        data[key] = newVal
      }
    })
  })(key)
}

# Object.defineProperty API 缺点

  • 不能监听对象属性新增和删除
  • 初始化阶段递归执行 Object.defineProperty 带来的性能负担

# Reactive API

  • 响应式的实现方式就是 劫持数据,Vue.js 3.0 的 Reactive API 就是通过 Proxy 劫持数据,由于 Proxy 劫持的是整个对象,所以可以检测到任何对对象的修改,弥补了 Object.defineProperty API 的不足。

# vue中如何解析模板

  • 模板是什么?
  1. 本质:字符串

  2. 有逻辑,如v-if``v-for

  3. html格式很像,但有很大区别

  4. 最终还是要转换为html来显示

  5. 模板最终必须转成JS代码,因为:

    a. 有逻辑(v-if v-for),必须用JS才能实现(图灵完备)

    b. 转换为html渲染页面,必须用JS才能实现

    c. 因此,模板最重要转换成一个JS函数(render函数

  • render函数
<div id="app">
  <p>{{price}}</p>
</div>
with(this){
  return _c(
    'div',
    {
      attrs: {"id":"app"}
    },
    [
      _c('p', [_v(_s(price))])
    ]
  )
}
  1. 模板中所有信息都包含在了render函数中

  2. thisvm

  3. pricethis.pricevm.price,即data中的price

  4. _cthis._cvm._c

  • render函数执行时返回vnode

a. updateComponentt中实现了vdompath

b. 页面首次渲染执行updateComponent

c. data中每次修改属性,执行updateComponent

# vue的整个实现流程

  1. 解析模板成render函数

    a. 模板中所有信息都被render函数所包含

    b. 模板中用到的data中的属性,都变成了JS变量

    c. 模板中的v-model v-for都变成了JS逻辑

    d. render函数返回vnode

  2. 响应式开始监听

    a. vue中数据驱动视图改变,其核心就是通过Object.defineProperty实时监听到数据的变化

    b. 同样,通过Object.defineProperty,将data的属性代理到vm

  3. 首次渲染,显示页面,且绑定依赖

    a. 初次渲染,执行updateComponent,执行vm._render()

    b. 执行render函数,会访问到上一步代理到vm上的data的属性

    c. 会被响应式的get方法监听到

    因为,data中有很多属性,有些被用到,有些可能不被用到,被用到的走get,不被用到的不会走到get,未走到get中的属性,set的时候我们也无需关心,避免不必要的重复渲染

    d. 执行updateComponent,会走到vdompatch方法

    e. patchvnode渲染成DOM,初次渲染完成

  4. data属性变化,触发rerender

    a. 修改属性,被响应式的set监听到

    b. set中执行updateComponent

    c. updateComponent重新执行vm._render()

    d. 生成的vnodepreVnode,通过patch进行对比

    e. 渲染到html

# 入口

vue首先是一个Function,es5中通过方法声明一个类,需要new这个方法,也就是new Vue(),生成一个实例对象,这个实例对象上有那么多方法哪来的呢?是通过Vue的prototype挂载到原型上。

这里没有通过ES6的class来定义,原因就是js是动态类型语言,通过方法,我们可以随意挂载方法到它的原型上,这样一类方法用一个文件来编写,就把代码解耦了,做到了模块化拆分。

# vue的数据驱动

vue是怎么通过data的赋值就实现了DOM的渲染呢?并没有像jq那样直接操作dom啊?下面就是答案:

在Vue中,我们通过$mount实例方法来挂载vm

首先,对el做了限制,Vue不能挂载到bodyhtml上这样的根节点上,然后会进行判断,如果没有定义render方法,则会把el或者template字符串转换成render方法。

$mount方法传入两个参数,第一个是el,表示挂载的元素,可以是字符串,也可以是DOM对象,如果是字符串在浏览器环境下会调用query方法来转成DOM对象,第二个参数是和服务端渲染(ssr)有关。

然后就是调用vm._render方法了生成虚拟的Node了,同时呢,在实例化一个Watcher,也就是说在页面初始化的时候通过一大堆的数据计算后,渲染DOM,然后呢,中间数据变化了,Watcher监听到了,再次执行。

第一步,把一堆template转成render函数;

第二步,通过render中的createElement创建组件的VNode(虚拟DOM)

第三步,走vm._update调用__patch__,把虚拟DOM转换成真正的DOM节点。

update的调用时机有两个,一个是首次渲染,一个是数据更新的时候,通过VNODE Tree调用js原生的appendChild方法,调用insert方法把DOM插入到父节点,进行递归调用,把虚拟dom和真实dom映射到一起,从而实现DOM的真正渲染。

vdom的真正意义是为了实现跨平台,服务端渲染,以及提供一个性能还算不错 Dom 更新策略 vdom 让整个 mvvm 框架灵活了起来,性能不是最重要的 好的 diff 算法只是 vdom 实现层面的优化,而并不代表 vdom 本身是为了性能优化

# runtime-only和compiler and runtime

vue实例挂载,最终都要通过render函数来执行,如果采用runtime-only模式,要直接写render函数,而采用compiler组合的模式,则可以书写template。通过wm._update方法传入render来渲染Watcher

# component生成

组件的生成的流程,是之前通过render方法,来createElement,如果tag是传入的最上层的app组件,那么首先Vue会构造子类的构造函数,把自身的一堆options传递给子类,组件集成过程其实用的就是

  Sub.prototype = Object.create(Super.prototype)
  Sub.prototype.constructor = Sub

Super完全继承了Vue的各种属性,所以Vue.extend也可以用组件componentName.extent来表示。

这里面有一个缓存方法,把构造函数进行了缓存,对于拥有相同父类的子组件,就不重复构造了。完了之后,通过new VNode实例化一个vnode并返回。

# 派发更新和nextTick

  • 派发更新就是当数据发生改变后,通知所有订阅了这个数据变化的watcher执行update

  • 派发更新的过程中会把所有要执行updatewatcher推入队列中,在nextTick后执行flush

  • 把所有的数据变化都收集到一次,做一次nextTick

  • 数据的变化到dom的变化是一个异步的过程

  • 本次nextTick如果没有执行到,会被收集到callbacks数组中,下次nextTick执行

  • nextTick返回一个promise对象

  • nextTick是把要执行的任务推入到一个队列中,在下一个tick同步执行