>  기사  >  웹 프론트엔드  >  Vue 소스 코드의 파일 구조 및 작동 메커니즘

Vue 소스 코드의 파일 구조 및 작동 메커니즘

不言
不言원래의
2018-07-09 11:22:401405검색

이 글은 Vue 소스 코드의 파일 구조와 작동 메커니즘을 주로 소개합니다. 이제 필요한 친구들이 참고할 수 있도록 공유합니다.

vue는 지금은 국내 프론트엔드 웹엔드가 전 세계의 3분의 1을 차지하고 있고, 일상적으로 사용하는 기술 스택 중 하나이기도 하고, 게다가 Vue 소스가 많은 이유도 궁금합니다. 최근 커뮤니티에 코드 읽기 기사가 올라왔습니다. 아래에서 빌려왔습니다. 이번 기회에 모든 분들의 기사와 토론에서 약간의 영양분을 끌어내는 동시에 소스 코드를 읽을 때의 몇 가지 생각을 요약하여 일부 기사를 작성합니다. 제 수준에는 한계가 있으니 토론메세지를 남겨주세요~

#🎜🎜 #Target Vue 버전: 2.5.17-beta.0
Vue 소스코드 댓글: https://github.com/SHERlocked...
진술: 기사의 소스 코드 구문 그들은 모두 Flow를 사용하며 소스 코드는 필요에 따라 삭제됩니다(혼란을 피하기 위해@_@ ). 풀버전을 보시려면 위의 github 주소를 입력해주세요. 본 글은 연재글 주소는 하단에 있습니다~

0. ul class=" list-paddingleft-2">

  • 2.5.17-beta.0
    vue源码注释:https://github.com/SHERlocked...
    声明:文章中源码的语法都使用 Flow,并且源码根据需要都有删节(为了不被迷糊 @_@),如果要看完整版的请进入上面的github地址,本文是系列文章,文章地址见底部~

    0. 前备知识

    • Flow

    • ES6语法

    • 常用的设计模式

    • 柯里化等函数式编程思想

    这里推介几篇前备文章:JS 静态类型检查工具 Flow,ECMAScript 6 入门 - 阮一峰,JS中的柯里化,JS 观察者模式,JS 利用高阶函数实现函数缓存(备忘模式)

    1. 文件结构

    文件结构在vue的CONTRIBUTING.md中有介绍,这边直接翻译过来:

    ├── scripts ------------------------------- 包含与构建相关的脚本和配置文件
    │   ├── alias.js -------------------------- 源码中使用到的模块导入别名
    │   ├── config.js ------------------------- 项目的构建配置
    ├── build --------------------------------- 构建相关的文件,一般情况下我们不需要动
    ├── dist ---------------------------------- 构建后文件的输出目录
    ├── examples ------------------------------ 存放一些使用Vue开发的应用案例
    ├── flow ---------------------------------- JS静态类型检查工具[Flow](https://flowtype.org/)的类型声明
    ├── package.json
    ├── test ---------------------------------- 测试文件
    ├── src ----------------------------------- 源码目录
    │   ├── compiler -------------------------- 编译器代码,用来将 template 编译为 render 函数
    │   │   ├── parser ------------------------ 存放将模板字符串转换成元素抽象语法树的代码
    │   │   ├── codegen ----------------------- 存放从抽象语法树(AST)生成render函数的代码
    │   │   ├── optimizer.js ------------------ 分析静态树,优化vdom渲染
    │   ├── core ------------------------------ 存放通用的,平台无关的运行时代码
    │   │   ├── observer ---------------------- 响应式实现,包含数据观测的核心代码
    │   │   ├── vdom -------------------------- 虚拟DOM的 creation 和 patching 的代码
    │   │   ├── instance ---------------------- Vue构造函数与原型相关代码
    │   │   ├── global-api -------------------- 给Vue构造函数挂载全局方法(静态方法)或属性的代码
    │   │   ├── components -------------------- 包含抽象出来的通用组件,目前只有keep-alive
    │   ├── server ---------------------------- 服务端渲染(server-side rendering)的相关代码
    │   ├── platforms ------------------------- 不同平台特有的相关代码
    │   │   ├── weex -------------------------- weex平台支持
    │   │   ├── web --------------------------- web平台支持
    │   │   │   ├── entry-runtime.js ---------------- 运行时构建的入口
    │   │   │   ├── entry-runtime-with-compiler.js -- 独立构建版本的入口
    │   │   │   ├── entry-compiler.js --------------- vue-template-compiler 包的入口文件
    │   │   │   ├── entry-server-renderer.js -------- vue-server-renderer 包的入口文件
    │   ├── sfc ------------------------------- 包含单文件组件.vue文件的解析逻辑,用于vue-template-compiler包
    │   ├── shared ---------------------------- 整个代码库通用的代码

    几个重要的目录:

    • compiler:编译,用来将template转化为render函数

    • core:Vue的核心代码,包括响应式实现、虚拟DOM、Vue实例方法的挂载、全局方法、抽象出来的通用组件等

    • platform:不同平台的入口文件,主要是 web 平台和 weex 平台的,不同平台有其特殊的构建过程,当然我们的重点是 web 平台

    • server:服务端渲染(SSR)的相关代码,SSR 主要把组件直接渲染为 HTML 并由 Server 端直接提供给 Client 端

    • sfc:主要是 .vue 文件解析的逻辑

    • shared:一些通用的工具方法,有一些是为了增加代码可读性而设置的

    其中在platform下src/platforms/web/entry-runtime.js文件作为运行时构建的入口,ESM方式输出 dist/vue.runtime.esm.js,CJS方式输出 dist/vue.runtime.common.js,UMD方式输出 dist/vue.runtime.js,不包含模板 template 到 render 函数的编译器
    src/platforms/web/entry-runtime-with-compiler.js文件作为运行时构建的入口,ESM方式输出 dist/vue.esm.js,CJS方式输出 dist/vue.common.js,UMD方式输出 dist/vue.js,包含compiler

    2. 入口文件

    任何前端项目都可以从 package.json 文件看起,先来看看它的 script.dev 就是我们运行 npm run dev 的时候它的命令行:

    "scripts": {
        "dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev"
    }
    这里的 rollup 是一个类似于 webpack 的JS模块打包器,事实上 Vue - v1.0.10 版本之前用的还是 webpack ,其后改成了 rollup ,如果想知道为什么换成 rollup ,可以看看 尤雨溪本人的回答,总的来说就是为了打出来的包体积小一点,初始化速度快一点。

    可以看到这里 rollup 去运行 scripts/config.js 文件,并且给了个参数 TARGET:web-full-dev,那来看看 scripts/config.js 里面是啥

    // scripts/config.js
    
    const builds = {
      'web-full-dev': {
        entry: resolve('web/entry-runtime-with-compiler.js'),  // 入口文件
        dest: resolve('dist/vue.js'),                          // 输出文件
        format: 'umd',                                         // 参看下面的编译方式说明
        env: 'development',                                    // 环境
        alias: { he: './entity-decoder' },                     // 别名
        banner                                        // 每个包前面的注释-版本/作者/日期.etc
      },
    }

    format 编译方式说明:
    es:ES Modules,使用ES6的模板语法输出
    cjs:CommonJs Module,遵循CommonJs Module规范的文件输出
    amd:AMD Module,遵循AMD Module规范的文件输出
    umd:支持外链规范的文件输出,此文件可以直接使用script标签

    这里的 web-full-dev 就是对应刚刚我们在命令行里传入的命令,那么 rollup 就会按下面的 entry 入口文件开始去打包,还有其他很多命令和其他各种输出方式和格式可以自行查看一下源码。

    因此本文主要的关注点在包含 compiler 编译器的 src/platforms/web/entry-runtime-with-compiler.js 文件,在生产和开发环境中我们使用 vue-loader 来进行 template 的编译从而不需要带 compiler 的包,但是为了更好的理解原理和流程还是推介从带 compiler 的入口文件看起。

    先看看这个文件,这里导入了个 Vue ,看看它从哪来的

    // src/platforms/web/entry-runtime-with-compiler.js
    
    import Vue from './runtime/index'

    继续看

    // src/platforms/web/runtime/index.js
    
    import Vue from 'core/index'

    keep moving

    // src/core/index.js
    
    import Vue from './instance/index'

    keep moving*2

    // src/core/instance/index.js
    
    /* 这里就是vue的构造函数了,不用ES6的Class语法是因为mixin模块划分的方便 */
    function Vue(options) {
      this._init(options)         // 初始化方法,位于 initMixin 中
    }
    
    // 下面的mixin往Vue.prototype上各种挂载
    initMixin(Vue)
    stateMixin(Vue)
    eventsMixin(Vue)
    lifecycleMixin(Vue)
    renderMixin(Vue)
    
    export default Vue

    当我们 new Vue( )Flow

  • #🎜🎜#ES6 구문#🎜 🎜#
  • #🎜🎜 #일반적으로 사용되는 디자인 패턴#🎜🎜#
  • #🎜🎜#currying과 같은 기능적 프로그래밍 아이디어#🎜🎜#
  • ul>#🎜🎜#여기에 몇 가지 예비 기사가 있습니다: JS static 유형 검사 도구 흐름, ECMAScript 6 소개 - Ruan Yifeng, JS에서 currying, JS 관찰자 모드, JS에서 고차 함수를 사용하여 함수 캐싱 구현(준비됨) Forgot 모드)#🎜🎜#

    파일 구조

    #🎜🎜#파일 구조는 vue의 CONTRIBUTING.md에 소개되어 있으며 여기에서 직접 번역됩니다: #🎜🎜#
    // src/core/instance/index.js
    
    /* 这里就是Vue的构造函数 */
    function Vue(options) {
      this._init(options)              // 初始化方法,位于 initMixin 中
    }
    
    // 下面的mixin往Vue.prototype上各种挂载,这是在加载的时候已经挂载好的
    initMixin(Vue)                     // 给Vue.prototype添加:_init函数,...
    stateMixin(Vue)                    // 给Vue.prototype添加:$data属性, $props属性, $set函数, $delete函数, $watch函数,...
    eventsMixin(Vue)                   // 给Vue.prototype添加:$on函数, $once函数, $off函数, $emit函数, $watch方法,...
    lifecycleMixin(Vue)                // 给Vue.prototype添加: _update方法, $forceUpdate函数, $destroy函数,...
    renderMixin(Vue)                   // 给Vue.prototype添加: $nextTick函数, _render函数,...
    
    export default Vue
    #🎜🎜#a some 중요한 디렉터리: #🎜🎜#
    • #🎜🎜#컴파일러:템플릿을 렌더링 함수로 변환하는 데 사용되는 컴파일#🎜🎜 #
    • #🎜🎜#코어:반응형 구현, 가상 DOM, Vue 인스턴스 메서드 마운팅, 전역 메서드, 추상화된 공통 구성 요소 등을 포함한 Vue의 핵심 코드 #🎜🎜#
    • #🎜🎜#플랫폼: Strong>다양한 플랫폼의 항목 파일은 주로 웹 플랫폼과 weex 플랫폼용입니다. 플랫폼마다 고유한 구성 프로세스가 있습니다. 물론 우리는 웹 플랫폼에 중점을 두고 있습니다 #🎜🎜#
    • #🎜 🎜#서버: 서버측 렌더링(SSR) 관련 코드입니다. SSR은 주로 구성 요소를 HTML로 직접 렌더링하여 서버에서 사용합니다. 클라이언트가 클라이언트에 직접 제공됩니다#🎜🎜#
    • #🎜🎜#sfc:주로 .vue 파일 구문 분석 논리#🎜🎜#
    • # 🎜🎜#공유:몇 가지 일반적인 도구 메소드 중 일부는 코드 가독성을 높이기 위해 설정되어 있습니다#🎜🎜#
    #🎜🎜#플랫폼 아래 src/platforms/web/entry-runtime.js 파일은 런타임 빌드의 진입점 역할을 하며, CJS 모드는 dist/vue.runtime.common을 출력하고, UMD 모드는 dist/vue.runtime을 출력합니다. Node.js는 컴파일러
    src/platforms/web/entry-runtime-with-compiler.js 파일의 렌더링 기능에 템플릿 템플릿을 포함하지 않습니다. 런타임 빌드의 입구로서, ESM 모드는 dist/vue.esm.js를 출력하고, CJS 모드는 dist/vue.common.js를 출력하며, UMD 모드는 컴파일러#🎜🎜#

    2 입구 파일을 포함하여 dist/vue.js를 출력합니다. h2>#🎜🎜#모든 프런트엔드 프로젝트는 package.json 파일에서 시작할 수 있습니다. 먼저 script.dev를 살펴보겠습니다. code >npm run dev 해당 명령줄: #🎜🎜#
    // src/core/instance/index.js
    
    Vue.prototype._init = function(options?: Object) {
      const vm: Component = this
    
      initLifecycle(vm)                     // 初始化生命周期 src/core/instance/lifecycle.js
      initEvents(vm)                        // 初始化事件 src/core/instance/events.js
      initRender(vm)                        // 初始化render src/core/instance/render.js
      callHook(vm, 'beforeCreate')          // 调用beforeCreate钩子
      initInjections(vm)                    // 初始化注入值 before data/props src/core/instance/inject.js
      initState(vm)                         // 挂载 data/props/methods/watcher/computed
      initProvide(vm)                       // 初始化Provide after data/props
      callHook(vm, 'created')               // 调用created钩子
    
      if (vm.$options.el) {                    // $options可以认为是我们传给 `new Vue(options)` 的options
        vm.$mount(vm.$options.el)              // $mount方法
      }
    }
    여기의 롤업은 실제로 webpack과 유사한 JS 모듈 패키저입니다. Vue - v1.0.10 버전은 webpack을 사용했지만 나중에 롤업으로 변경되었습니다. 왜 롤업으로 변경되었는지 알고 싶다면 You Yuxi의 답변을 읽어보세요. 일반적으로 패키지를 더 작게 만들고 초기화 속도를 조금 더 빠르게 만드는 것입니다. .
    #🎜🎜#여기서 롤업이 scripts/config.js 파일을 실행하고 TARGET:web-full-dev 매개변수를 제공하는 것을 볼 수 있습니다. scripts/config.js#🎜🎜#
    // src/platform/web/entry-runtime-with-compiler.js
    
    const mount = Vue.prototype.$mount    // 把原来的$mount保存下来,位于 src/platform/web/runtime/index.js
    Vue.prototype.$mount = function(
      el?: string | Element,    // 挂载的元素
      hydrating?: boolean       // 服务端渲染相关参数
    ): Component {
      el = el && query(el)
      
      const options = this.$options
      if (!options.render) {                // 如果没有定义render方法
        let template = options.template
        
        // 把获取到的template通过编译的手段转化为render函数
        if (template) {
          const { render, staticRenderFns } = compileToFunctions(template, {...}, this)
          options.render = render
        }
      }
      return mount.call(this, el, hydrating)      // 执行原来的$mount
    }
    #🎜🎜#format 안에 무엇이 있는지 살펴보세요. 컴파일 방법 설명:
    es:ES 모듈, ES6 사용 템플릿 구문 출력
    cjs:CommonJs 모듈, CommonJs 모듈 사양을 따르는 파일 출력
    amd:AMD 모듈, AMD 모듈을 따르는 파일 출력 사양
    umd: 외부 링크 사양 파일 출력을 지원합니다. 이 파일은 web-full-dev에서 스크립트 태그 #🎜🎜##🎜🎜#를 직접 사용할 수 있습니다. 방금 명령줄에 전달한 명령에 따라 롤업은 아래 항목 항목 파일에 따라 패키징을 시작합니다. 그 밖에도 다양한 명령과 다양한 출력 방법 및 형식이 있으며 소스 코드를 직접 확인할 수 있습니다. #🎜🎜##🎜🎜# 따라서 이 기사의 주요 초점은 프로덕션 및 개발 환경 템플릿을 컴파일하기 위해 vue-loader를 사용하며 컴파일러가 있는 패키지는 필요하지 않습니다. 그러나 원리와 프로세스를 더 잘 이해하려면 컴파일러를 사용하여 항목 파일부터 시작하는 것이 좋습니다. #🎜🎜##🎜🎜#먼저 이 파일을 살펴보겠습니다. Vue는 어디에서 왔는지 확인하기 위해 여기로 가져옵니다. new Vue( )를 사용하면 실제로 이 생성자를 호출할 수 있으며 여기서 시작할 수 있습니다. #🎜🎜#

    3. 运行机制

    这里我用xmind粗略的画了一张运行机制图,基本上后面的分析都在这张图上面的某些部分了

    本文 Vue 实例都是用 vm 来表示

    Vue 소스 코드의 파일 구조 및 작동 메커니즘

    上面这个图可以分为多个部分细加阅读,具体的实现我们在后面的文章中详细讨论,这里先贴一部分源码尝尝鲜

    3.1 初始化 _init( )

    Vue 소스 코드의 파일 구조 및 작동 메커니즘

    当我们在 main.js 里 new Vue( ) 后,Vue 会调用构造函数的 _init( ) 方法,这个方法是位于 core/instance/index.js 的 initMixin( ) 方法中定义的

    // src/core/instance/index.js
    
    /* 这里就是Vue的构造函数 */
    function Vue(options) {
      this._init(options)              // 初始化方法,位于 initMixin 中
    }
    
    // 下面的mixin往Vue.prototype上各种挂载,这是在加载的时候已经挂载好的
    initMixin(Vue)                     // 给Vue.prototype添加:_init函数,...
    stateMixin(Vue)                    // 给Vue.prototype添加:$data属性, $props属性, $set函数, $delete函数, $watch函数,...
    eventsMixin(Vue)                   // 给Vue.prototype添加:$on函数, $once函数, $off函数, $emit函数, $watch方法,...
    lifecycleMixin(Vue)                // 给Vue.prototype添加: _update方法, $forceUpdate函数, $destroy函数,...
    renderMixin(Vue)                   // 给Vue.prototype添加: $nextTick函数, _render函数,...
    
    export default Vue

    我们可以看看 init( ) 这个方法到底进行了哪些初始化:

    // src/core/instance/index.js
    
    Vue.prototype._init = function(options?: Object) {
      const vm: Component = this
    
      initLifecycle(vm)                     // 初始化生命周期 src/core/instance/lifecycle.js
      initEvents(vm)                        // 初始化事件 src/core/instance/events.js
      initRender(vm)                        // 初始化render src/core/instance/render.js
      callHook(vm, 'beforeCreate')          // 调用beforeCreate钩子
      initInjections(vm)                    // 初始化注入值 before data/props src/core/instance/inject.js
      initState(vm)                         // 挂载 data/props/methods/watcher/computed
      initProvide(vm)                       // 初始化Provide after data/props
      callHook(vm, 'created')               // 调用created钩子
    
      if (vm.$options.el) {                    // $options可以认为是我们传给 `new Vue(options)` 的options
        vm.$mount(vm.$options.el)              // $mount方法
      }
    }

    这里 _init() 方法中会对当前 vm 实例进行一系列初始化设置,比较重要的是初始化 State 的方法 initState(vm) 的时候进行 data/props 的响应式化,这就是传说中的通过 Object.defineProperty() 方法对需要响应式化的对象设置 getter/setter,以此为基础进行依赖搜集(Dependency Collection),达到数据变化驱动视图变化的目的。

    最后检测 vm.$options 上面有没有 el 属性,如果有的话使用 vm.$mount 方法挂载 vm,形成数据层和视图层的联系。这也是如果没有提供 el 选项就需要自己手动 vm.$mount('#app') 的原因。

    我们看到 created 钩子是在挂载 $mount 之前调用的,所以我们在 created 钩子触发之前是无法操作 DOM 的,这是因为还没有渲染到 DOM 上。

    3.2 挂载 $mount( )

    Vue 소스 코드의 파일 구조 및 작동 메커니즘

    挂载方法 vm.$mount( ) 在多个地方有定义,是根据不同打包方式和平台有关的,src/platform/web/entry-runtime-with-compiler.jssrc/platform/web/runtime/index.jssrc/platform/weex/runtime/index.js,我们的关注点在第一个文件,但在 entry-runtime-with-compiler.js 文件中会首先把 runtime/index.js 中的 $mount 方法保存下来,并在最后用 call 运行:

    // src/platform/web/entry-runtime-with-compiler.js
    
    const mount = Vue.prototype.$mount    // 把原来的$mount保存下来,位于 src/platform/web/runtime/index.js
    Vue.prototype.$mount = function(
      el?: string | Element,    // 挂载的元素
      hydrating?: boolean       // 服务端渲染相关参数
    ): Component {
      el = el && query(el)
      
      const options = this.$options
      if (!options.render) {                // 如果没有定义render方法
        let template = options.template
        
        // 把获取到的template通过编译的手段转化为render函数
        if (template) {
          const { render, staticRenderFns } = compileToFunctions(template, {...}, this)
          options.render = render
        }
      }
      return mount.call(this, el, hydrating)      // 执行原来的$mount
    }

    在 Vue 2.0 版本中,所有 Vue 的组件的渲染最终都需要 render 方法,无论我们是用单文件 .vue 方式开发组件,还是写了 el 或者 template 属性,最终都会转换成 render 方法。这里的 compileToFunctions 就是把 template 编译为 render 的方法,后面会介绍。

    // src/platform/weex/runtime/index.js
    
    Vue.prototype.$mount = function (
      el?: string | Element,    // 挂载的元素
      hydrating?: boolean       // 服务端渲染相关参数
    ): Component {
      el = el && inBrowser ? query(el) : undefined        // query就是document.querySelector方法
      return mountComponent(this, el, hydrating)          // 位于core/instance/lifecycle.js
    }

    这里的 el 一开始如果不是DOM元素的话会被 query 方法换成DOM元素再被传给 mountComponent 方法,我们继续看 mountComponent 的定义:

    // src/core/instance/lifecycle.js
    
    export function mountComponent (
      vm: Component,
      el: ?Element,
      hydrating?: boolean
    ): Component {
      vm.$el = el
      if (!vm.$options.render) {
        vm.$options.render = createEmptyVNode
      }
      callHook(vm, 'beforeMount')            // 调用beforeMount钩子
    
      // 渲染watcher,当数据更改,updateComponent作为Watcher对象的getter函数,用来依赖收集,并渲染视图
      let updateComponent
      updateComponent = () => {
        vm._update(vm._render(), hydrating)
      }
    
      // 渲染watcher, Watcher 在这里起到两个作用,一个是初始化的时候会执行回调函数
      // ,另一个是当 vm 实例中的监测的数据发生变化的时候执行回调函数
      new Watcher(vm, updateComponent, noop, {
        before () {
          if (vm._isMounted) {
            callHook(vm, 'beforeUpdate')            // 调用beforeUpdate钩子
          }
        }
      }, true /* isRenderWatcher */)
    
      // 这里注意 vm.$vnode 表示 Vue 实例的父虚拟 Node,所以它为 Null 则表示当前是根 Vue 的实例
      if (vm.$vnode == null) {
        vm._isMounted = true               // 表示这个实例已经挂载
        callHook(vm, 'mounted')            // 调用mounted钩子
      }
      return vm
    }

    mountComponent 方法里实例化了一个渲染 Watcher,并且传入了一个 updateComponent ,这个方法:() => { vm._update(vm._render(), hydrating) } 首先使用 _render 方法生成 VNode,再调用 _update 方法更新DOM。可以看看视图更新部分的介绍

    这里调用了几个钩子,他们的时机可以关注一下。

    3.3 编译 compile( )

    如果在需要转换 render 的场景下,比如我们写的 template ,将会被 compiler 转换为 render 函数,这其中会有几个步骤组成:

    Vue 소스 코드의 파일 구조 및 작동 메커니즘

    入口位于刚刚 src/platform/web/entry-runtime-with-compiler.js 的 compileToFunctions 方法:

    // src/platforms/web/compiler/index.js
    
    const { compile, compileToFunctions } = createCompiler(baseOptions)
    export { compile, compileToFunctions }

    继续看这里的 createCompiler 方法:

    // src/compiler/index.js
    
    export const createCompiler = createCompilerCreator(function baseCompile (
      template: string,
      options: CompilerOptions
    ): CompiledResult {
      const ast = parse(template.trim(), options)
      if (options.optimize !== false) {
        optimize(ast, options)
      }
      const code = generate(ast, options)
      return {
        ast,
        render: code.render,
        staticRenderFns: code.staticRenderFns
      }
    })

    这里可以看到有三个重要的过程 parseoptimizegenerate,之后生成了 render 方法代码。

    • parse:会用正则等方式解析 template 模板中的指令、class、style等数据,形成抽象语法树 AST

    • optimize:优化AST,生成模板AST树,检测不需要进行DOM改变的静态子树,减少 patch 的压力

    • generate:把 AST 生成 render 方法的代码

    3.4 响应式化 observe( )

    Vue作为一个MVVM框架,我们知道它的 Model 层和 View 层之间的桥梁 ViewModel 是做到数据驱动的关键,Vue的响应式是通过 Object.defineProperty 来实现,给被响应式化的对象设置 getter/setter ,当 render 函数被渲染的时候会触发读取响应式化对象的 getter 进行依赖收集,而在修改响应式化对象的时候会触发设置 settersetter 方法会 notify 它之前收集到的每一个 watcher 来告诉他们自己的值更新了,从而触发 watcherupdatepatch 更新视图。

    Vue 소스 코드의 파일 구조 및 작동 메커니즘

    响应式化的入口位于 src/core/instance/init.js 的 initState 中:

    // src/core/instance/state.js
    
    export function initState(vm: Component) {
      vm._watchers = []
      const opts = vm.$options
      if (opts.props) initProps(vm, opts.props)
      if (opts.methods) initMethods(vm, opts.methods)
      if (opts.data) {
        initData(vm)
      } else {
        observe(vm._data = {}, true /* asRootData */)
      }
      if (opts.computed) initComputed(vm, opts.computed)
      if (opts.watch && opts.watch !== nativeWatch) {
        initWatch(vm, opts.watch)
      }
    }

    它非常规律的定义了几个方法来初始化 propsmethodsdatacomputedwathcer,这里只看 initData 方法,来窥一豹

    // src/core/instance/state.js
    
    function initData(vm: Component) {
      let data = vm.$options.data
      data = vm._data = typeof data === 'function'
                        ? getData(data, vm)
                        : data || {}
      
      observe(data, true /* asRootData */) // 给data做响应式处理
    }

    首先判断了下 data 是不是函数,是则取返回值不是则取自身,之后有一个 observe 方法对 data 进行处理,看看这个方法

    // src/core/observer/index.js
    
    export function observe (value: any, asRootData: ?boolean): Observer | void {
      let ob: Observer | void
      ob = new Observer(value)
      return ob
    }

    这个方法主要用 data 去实例化一个 Observer 对象实例,Observer 是一个 Class,Observer 的构造函数使用 defineReactive 方法给对象的键响应式化,它给对象的属性递归添加 getter/setter,用于依赖收集和 notify 更新,这个方法大概是这样的

    // src/core/observer/index.js
    
    function defineReactive (obj, key, val) {
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get: function reactiveGetter () {
                /* 进行依赖收集 */
                return val;
            },
            set: function reactiveSetter (newVal) {
                if (newVal === val) return;
                notify();                // 触发更新
            }
        });
    }

    3.5 视图更新 patch( )

    Vue 소스 코드의 파일 구조 및 작동 메커니즘

    当使用 defineReactive 方法将对象响应式化后,当 render 函数被渲染的时候,会读取响应化对象的 getter 从而触发 getter 进行 watcher 依赖的收集,而在修改响应化对象的值的时候,会触发 setter 通知 notify 之前收集的依赖,通知自己已被修改,请按需重新渲染视图。被通知的 watcher 调用 update 方法去更新视图,位于上面介绍过的传递给 new Watcher( )updateComponent 方法中,这个方法会调用 update 方法去 patch 更新视图。

    // src/core/instance/lifecycle.js
    
    let updateComponent
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
    
    // 渲染watcher, Watcher 在这里起到两个作用,一个是初始化的时候会执行回调函数
    // ,另一个是当 vm 实例中的监测的数据发生变化的时候执行回调函数
    new Watcher(vm, updateComponent, noop, {...}, true /* isRenderWatcher */)

    这个 _render 方法生成虚拟 Node, _update 方法中的会将新的 VNode 与旧的 VNode 一起传入 patch

    // src/core/instance/lifecycle.js
    
    Vue.prototype._update = function(vnode: VNode, hydrating?: boolean) { // 调用此方法去更新视图
      const vm: Component = this
      const prevVnode = vm._vnode
      vm._vnode = vnode
    
      if (!prevVnode) {
        // 初始化
        vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
      } else {
        //更新
        vm.$el = vm.__patch__(prevVnode, vnode)
      }
    }

    _update 调用 __patch__ 方法,它主要是对新老 VNode 进行比较  patchVnode,经过 diff 算法得出它们的差异,最后这些差异的对应 DOM 进行更新。

    以上就是本文的全部内容,希望对大家的学习有所帮助,更多相关内容请关注PHP中文网!

    相关推荐:

    Vue源码之依赖收集原理

    关于react项目静态类型检查方案

    위 내용은 Vue 소스 코드의 파일 구조 및 작동 메커니즘의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

    성명:
    본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.