vue响应式原理|模版编译|虚拟DOM源码分析

904 阅读4分钟

最近翻读vue的源码,总结一下几点:

vue 首次渲染的过程

在源码中有4个导出vue的文件

src/core/instance/index.js


// 注册 vm 的 _init() 方法,初始化 vm
initMixin(Vue)
// 注册 vm 的 $data/$props/$set/$delete/$watch
stateMixin(Vue)
// 初始化事件相关方法
// $on/$once/$off/$emit
eventsMixin(Vue)
// 初始化生命周期相关的混入方法
// _update/$forceUpdate/$destroy
lifecycleMixin(Vue)
// 混入 render
// $nextTick/_render
renderMixin(Vue)

src/core/index.js

调用initGlobalAPI() 给vue的构造函数初始化静态成员

  • 设置 keep-alive 组件 extend(Vue.options.components, builtInComponents);
//  初始化 Vue.config 对象
Object.defineProperty(Vue, "config", configDef);

// 静态方法 set/delete/nextTick
Vue.set = set;
Vue.delete = del;
Vue.nextTick = nextTick;

// 初始化 Vue.options 对象,并给其扩展
// components/directives/filters
Vue.options = Object.create(null);
ASSET_TYPES.forEach((type) => {
  Vue.options[type + "s"] = Object.create(null);
});

Vue.options._base = Vue;

// 设置 keep-alive 组件
extend(Vue.options.components, builtInComponents);

// 注册 Vue.use() 用来注册插件
initUse(Vue);
// 注册 Vue.mixin() 实现混入
initMixin(Vue);
// 注册 Vue.extend() 基于传入的options返回一个组件的构造函数
initExtend(Vue);
// 注册 Vue.directive()、 Vue.component()、Vue.filter()
initAssetRegisters(Vue);

src/platforms/web/runtime/index.js

注册平台相关的属性和指令 注册Vue.prototype.patch,Vue.prototype.$mount

src/platforms/web/entry-runtime-with-compiler.js

重写$mount方法,新增了把 template/el 转换成 render 函数

总结渲染过程:

在new Vue()之前先初始化vue构造函数
  • 1、在src/core/instance/index.js文件中主要负责给vue的原型上挂载一些实例成员和属性
  • 2、在src/core/index.js文件中负责initGlobalAPI(Vye) 给vue的构造函数初始化静态成员
  • 3、src/platforms/web/runtime/index.js初始化和平台相关的内容 如:初始化指令,组件,patch,$mount方法
  • 4、src/platforms/web/entry-runtime-with-compiler.js是入口文件 重写$mount方法,新增了把 template/el 转换成 render 函数
初始化结束后调用new Vue(),在构造函数中调用_init()方法(_init方法是整个vue的入口)
  • 在_init()中定义了mount,mount,在mount中先判断是否传入render,调用comileToFunctions将模版编译成render函数,将render存入options选项中 options.render=render
  • 在$mount方法的最后调用mount.call(this, el, hydrating)方法; 也就是在runtime/index.js中定义的方法,改方法中重新获取el,调用mountComponent渲染DOM
在mountComponent中
  • 判断是否有render选项,如果没有但是传入了模版,并且当前是开发环境的话会发送警告
  • 在挂载之前调用callHook(vm,'beforeMount')触发钩子函数
  • 定义updateComponent函数 内部调用vm._update(vm._render(),hydrating)
    • vm._render()作用是调用用户传入的render()或编译器中的人的人(),返回虚拟DOM,
    • vm._update()作用是将虚拟DOM转换成真实DOM,更新到页面上
  • 之后创建一个 new Watcher(vm,updateComponent)对象
创建完watcher会调用一次get()
  • 调用updateComponent()方法
  • 调用vm._render()创建VNode
  • 调用vm._update(vnode),当中调用__patch__方法 挂载真实DOM,记录到vm.$el
  • 最后触发 callHook(vm,'mounted')挂载完毕 返回vm

vue响应式原理实现过程

src/core/instance/index.js中调用initState()方法

initState()中 初始化vue实例的状态

if(opts.data){
	initData(vm)//遍历data成员注入到vue实例
}else{
	observe(vm._Data={},true) //将data对象转换成响应式对象
}

observe(value)中

- 判断 value 是否是对象如果不是对象直接返回
- 判断value对象是否有__ob__,有直接返回
- 没有创建observer对象
- 返回observer对象
// 判断 value 是否是对象
if (!isObject(value) || value instanceof VNode) {
  return
}
// 如果 value 有 __ob__(observer对象) 属性 结束
// 如果没有ob,创建一个 Observer 对象
  ob = new Observer(value)
// 返回ob

Observer中

- 给value对象定义不可枚举的__ob__属性,记录当前的observer对象
- 数组的响应式处理
- 对象的响应式处理 调用walk方法
- 被附加到每一个对象,将目标对象的每个属性转换成getter和setter,目的是为了收集依赖和派发更新
constructor (value: any) {
  this.value = value
  this.dep = new Dep()
  // 初始化实例的 vmCount 为0
  this.vmCount = 0
  // 将实例挂载到观察对象的 __ob__ 属性
  def(value, '__ob__', this)
  // 数组的响应式处理
  if (Array.isArray(value)) {
    if (hasProto) {
      protoAugment(value, arrayMethods)
    } else {
      copyAugment(value, arrayMethods, arrayKeys)
    }
    // 为数组中的每一个对象创建一个 observer 实例
    this.observeArray(value)
  } else {
    // 遍历对象中的每一个属性,转换成 setter/getter
    this.walk(value)
  }
}

walk (obj: Object) {
  // 获取观察对象的每一个属性
  const keys = Object.keys(obj)
  // 遍历每一个属性,设置为响应式数据
  for (let i = 0; i < keys.length; i++) {
    defineReactive(obj, keys[i])
  }
}


observeArray (items: Array<any>) {
  for (let i = 0, l = items.length; i < l; i++) {
    observe(items[i])
  }
}

defineReactive中 src/core/observer/index.js

  • 为每一个属性创建dep对象
  • 如果当前属性是对象,调用observe
  • 定义getter 收集依赖 返回属性值
  • 定义setter 保存新值,如果新值是对象,调用observe;派发更新发送通知,调用dep.notify()
// 创建依赖对象实例
const dep = new Dep()
// 判断是否递归观察子对象,并将子对象属性都转换成 getter/setter,返回子观察对象
let childOb = !shallow && observe(val)
//定义get set
Object.defineProperty(obj, key, {
 enumerable: true,
 configurable: true,
 get:(){},
 set:(){},
})

收集依赖

- 在watcher对象的get方法中调用pushTarget记录Dep.target属性
- 访问data中的成员时候收集依赖,defineReactive的getter中收集依赖
- 把属性对应的wathcer对象添加到dep的subs数组中
- 个体childOb收集依赖,目的是子对象添加和删除成员时候发送通知
```js
// 如果存在当前依赖目标,即 watcher 对象,则建立依赖
  if (Dep.target) {
    dep.depend()
    // 如果子观察目标存在,建立子对象的依赖关系
    if (childOb) {
      childOb.dep.depend()
      // 如果属性是数组,则特殊处理收集数组对象依赖
      if (Array.isArray(value)) {
        dependArray(value)
      }
    }
  }
  ```

Watcher

  • dep.notify()调用wacher对象的update()方法
  • queueWatcher()判断wacher是否被处理,如果没有的话添加到queue队列,并调用flushSchedulerQueue() - flushSchedulerQueue()
    • 触发beforeUpdate钩子函数

    • 调用wacher.run() run()->get()->getter()->updateComponent

    • 清空上一次依赖

    • 触发actived钩子函数

    • 触发updated钩子函数

虚拟 DOM 中 Key 的作用和好处

可以减少 DOM 的操作

vue 模版编译的过程

首先调用$mount(),通过模版编译的入口函数 compilerToFunction 将 template 模版编译成 render 函数 ,compilerToFunction中主要实现:

读取缓存中的 CompiledFunctionResult 对象,如果有直接返回

调用compile(template, options) 把模板编译为编译对象(render, staticRenderFns),字符串形式的 js 代码

  • compile函数作用是:合并选项 调用baseCompile编译 记录错误返回编译好的对象
  • baseComipile中主要做三件事
    1. 把模板转换成 ast 抽象语法树
    const ast = parse(template.trim(), options);
    
    1. 优化抽象语法树

    • 标记AST tree中的静态sub trees
    • 检测到静态子树,设置为静态,不需要在每次重新渲染的时候重新生成节点
    • patch阶段跳过静态子树
    if (options.optimize !== false) {
      optimize(ast, options);
    }
    
    1. 把抽象语法树生成字符串形式的 js 代码
     const code =generate(ast, options);
    

调用createFunction把上一步中生成的字符串形式的 js 代码转换成 js 方法

render, staticRenderFns初始化完毕,挂载到Vue实例的options对应的属性中,缓存并返回res对象(render, staticRenderFns方法)