最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 如何开始阅读VUE源码

    正文概述 掘金(前端飘哥)   2021-06-30   465

    前言

    关于vue响应式的文章其实已经挺多了,不过大多都在浅尝辄止,基本就是简单介绍一下Object.defineProperty,覆盖一下setter做个小demo就算解决,好一点的会帮你引入observe、watcher、dep的概念,以及加入对Array的特殊处理,所以本篇除了上述以外,更多的重心将放在setter引发render的机制与流程上,然后结合这个这个响应式机制解析vue中的watchcomputed语法实现

    文章分为两部分,第一部分会简单介绍vue实例构建流程,第二部分则深入探究响应式实现。

    版本信息:

    • vue: 2.6.12

    一、寻找vue

    真正的vue实例在core/instance/index中可以找到

    function Vue (options) {
      ....
      this._init(options) // 这个方法在initMixin中定义
    }
    
    initMixin(Vue)  // 挂载_init()
    stateMixin(Vue)  // 挂载状态处理方法(挂载data,methods等)
    eventsMixin(Vue)  // 挂载 事件 的方法($on,$off等)
    lifecycleMixin(Vue) // 挂载 生命周期方法(update,destory)
    renderMixin(Vue)  // 挂载与渲染有关的方法($nextTick,_render) 
    

    每个方法可以按照代码逻辑来看,实现对应功能,这里拿initMixin举例

    initMixin

    initMixin中仅仅挂载了_init()方法,在_init中,初始化了整个vue的状态:

    function _init(option) {
      ...
      vm._uid = uid++ // 即component id
      ...
      initLifecycle(vm)
      initEvents(vm)
      initRender(vm)
      callHook(vm, 'beforeCreate')
      initInjections(vm) // resolve injections before data/props
      initState(vm)
      initProvide(vm)
      callHook(vm, 'created')
      ...
      if (vm.$options.el) {
        vm.$mount(vm.$options.el) // 开始挂载
      }
    } 
    

    这里我们可以看到几个beforeCreate,createdMount关键字,大概就能够猜到vue实例的部分生命周期方法就是在这里进行了挂载,再结合 vue官方文档的图示

    如何开始阅读VUE源码

    关于初始化整个vue的状态,可以举例来说,例如initLifecycle中就赋值了parent,children,以及一些isMounted,isDestroy的标识符。initRender中就将attrs,listeners响应化,等等,诸如此类。

    initMixin=>initState=>initData,便可以看到挂载props,methods,data,computed,watch了,

    initData中,先是获取了data数据,判断props,methods变量重名问题,然后是走了一个代理,将变量名代理到vue实例上,这样的话你的vue实例中,使用this.x指向就可以访问到this.data.x,这类代理也用在了propsmethods

    export function proxy (target: Object, sourceKey: string, key: string) {
      sharedPropertyDefinition.get = function proxyGetter () {
        return this[sourceKey][key]
      }
      sharedPropertyDefinition.set = function proxySetter (val) {
        this[sourceKey][key] = val
      }
      Object.defineProperty(target, key, sharedPropertyDefinition)
    } 
    

    这个逻辑处理的设计也是非常巧妙,他覆盖了实例中对该key的访问,使用settergetter将实际访问指向了this.data[key]

    走到最后,就是observe(data),也就是开始处理vue数据的双向绑定

    二、双向绑定

    不同于react的单向数据流,vue使用的双向绑定,单向数据流可以理解为当源头的数据发生变动则触发行为,当然这个变动是主动的,即你需要setState才能触发,而双向绑定则可以抽象为,每一个数据旁边都有一个监护人(一种处理逻辑),当数据发生变化,这个监护人就会响应行为,这个流程是被动发生的,只要该数据发生变动,就会通过监护人触发行为。

    如果你之前有过了解,大概就会知道,js每个数据的变动都是通过Object原型链中的setter去改变值,而如果你在他改变值之前,去通知监护人,就能够实现上述的逻辑,这一点很多博客文章都写的非常清楚了。

    接着第一部分的initData知道最后observe(data),这里开始正式处理响应式。

    2.1 前置条件

    前面一直提到,通过Object的原型链改变对象的默认行为:gettersetter,首先我们需要知道,在js中,读取一个对象的值并不是直接读取,而是通过Object的原型链上的默认行为getter拿到对应的值,而改变这种行为实际上是通过Object.defineProperty,来重新定义一个对象的gettersetter,在/src/core/observer/index.js中我们可以看一个defineReactive方法,他就是vue用来实现这种行为的方法,也是这个响应式的核心

    function defineReactive(obj, key, val, ... ) {
      // 此处需要保留getter、setter是因为,开发者可能自己基于defineProperty已经做过一层覆盖,
      // 而响应式又会覆盖一次,所以为了保留开发者自己的行为,此处需要兼容原有的getter、setter
      const getter = property && property.get // 拿到默认的getter、setter行为
      const setter = property && property.set
      Object.defineProperty(obj, key, {
        enumerable: true, // 是否可以被枚举出来(例如Object.keys(),for in)
        configurable: true, // 是否可以被配置,是否可以被删除
        get: function() {
          const value = getter ? getter.call(obj) : val
          ...
          return value
        }
        set: function(newVal) {
          ...
          setter.call(obj, newVal)
      	} 
      })
    } 
    

    2. 2响应式

    首先,我们猜想一下,双向绑定的行为,数据能够响应行为的变化,而行为又能够操作数据的改变,虽然有部分教程会让你站在数据的角度去理解这种行为,实际上,我们站在行为的角度上去理解是更加方便的。

    我们将一种行为定义为一个Watcher,他有可能是一个vue文件的template中的dom节点渲染行为,也有可能是computed的计算值行为,总之,我们从行为的角度出发,一个行为的发生,会伴随着对变量的读取(回想一下我们在vue文件中的templatehtml标签时,总是会使用{{obj.xxx}}来读取某个变量并渲染),我们想要实现,变量的改变也会带动这个行为的重新渲染,是不是我们只需要在首次行为发生的周期内,在读取某个变量时,在这个变量内记录这个Watcher,这样的话,下次变量的改变时,我只要触发我之前记录过的Watcher就行了。所以,我们只需要在一个Watcher发生时,将其挂载到一个公共变量上,这样在读取一个值的时候,记录这个公共变量,就能够实现上述操作。

    如何开始阅读VUE源码

    2.2.1 Watcher

    既然说到将一种行为定义为一个watcher,那么可以在/src/core/observer/watcher.js中看到Watcher的实体类,而我们之前一直所说的“行为”,实际上就是构造器的第二个参数expOrFn,可以有表达式或者函数读取的两种模式

    class Watcher {
     	constructor ( vm: Component, // vue实例
        expOrFn: string | Function, // 行为
        cb: Function, // 为watch服务
        options?: ?Object,
        isRenderWatcher?: boolean // 判断是否为渲染watcher, )
    } 
    

    接着来看一种最典型的watcher行为,在/src/core/instance/lifecycle.js中的moundComponent方法中,可以看到一个实例化watcher的方法

    new Watcher(vm, updateComponent, noop, {
        before () {
          if (vm._isMounted && !vm._isDestroyed) {
            callHook(vm, 'beforeUpdate')
          }
        }
      }, true /* isRenderWatcher */) 
    

    可以看到,他将updateComponent(可以抽象为渲染行为)传给Watcher,而在Watcher的实例化中,将会执行此方法,当然在执行之前,pushTarget(this),将这个watcher挂载到公共变量上而后开始执行渲染行为,

    class Watch {
      constructor(...) {
        ....
        if (typeof expOrFn === 'function') {
          this.getter = expOrFn
        }
        this.get();
      }
      get() {
        pushTarget(this) // 挂载行为至公共Target
        value = this.getter.call(vm, vm) // 开始执行行为,之所以会有返回值是为了computed服务
        popTarget() // 取消挂载,避免下次读取变量时又会绑定此行为
      }
    } 
    

    如何开始阅读VUE源码

    此时,如果此行为读取了某个响应式变量,那么该变量的getter将会存储公共变量target,当行为完成后就会取消行为的挂载,这个时候我们再回过头来看前面的defineReactive的逻辑

    function defineReactive(obj, key) {
      const dep = new Dep(); // 每个数据都有一个自己的存储列表
      const getter = property && property.get
      const setter = property && property.set
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
          const value = getter ? getter.call(obj) : val
          if (Dep.target) { // 判断公共变量中是否挂载了行为(watcher)
            dep.depend() // 将行为(watcher)加入dep(即此变量的存储行为列表)
            ...
          }
          return value
        },
        set: function reactiveSetter(newVal) {
          const value = getter ? getter.call(obj) : val
          if (newVal === value || (newVal !== newVal && value !== value)) {
            return // 判断变量没有变化,则直接返回(后两者判断则是因为NaN!==NaN的特性)
          }
          if (setter) {
            setter.call(obj, newVal) // 开始
          } else {
            val = newVal
          }
          dep.notify() // 通知自己这个数据的存储列表,数据发生改变,需要重新执行行为(watcher)
        }
       });
      } 
    

    这个时候就很清晰明了了,这就是很多博客文章所说的依赖收集,变量在get时通过公共变量Target收集依赖(也就是本文所说的行为),在set时,即变量数据发生改变时,触发更新notify;

    2.2.2 Computed

    前文有大致介绍computed的实现,实际上在介绍完Wacher之后就可以来详细介绍了,计算属性computed并没有实际的变量,他通过原型链覆盖创造了一个变量指向(src/core/instance/state.jsinitComputed),回忆一下computed的两种写法

    'fullName': function() {
      return this.firstName + this.secondeName;
    }
    'fullName': {
      get: function () {...},
      set: function() {...},
    } 
    

    我们再来看一下initComputed

    function initComputed (vm: Component, computed: Object) {
     const watchers = vm._computedWatchers = Object.create(null)
     for (const key in computed) {
       const userDef = computed[key]
       // 对照着computed的两种写法,就能理解为什么这里有这样的判断,
       const getter = typeof userDef === 'function' ? userDef : userDef.get
       watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        { lazy: true }
      )
       defineComputed(vm, key, userDef) // 通过defineProperty来创造一个挂载在vm上key(fullName)的指向
     }
    } 
    

    可以看到,他将computedgetter方法,作为Watcher的行为传递了进去,这样在执行getter时,可以将此行为绑定至过程中所读取到的变量(firstName),如此,再下次firstName发生改变时,就会触发此Watcher,重新运行getter方法,得到一个新的fullName的值(还记得前文class Watch中的value = this.getter.call(vm, vm)吗?这个返回值就是computed的返回值),这样就实现了computed的逻辑

    2.2.3 Watch

    watch的用法,是监听某个变量,当该变量发生变化时,执行特定的逻辑,

    上文提到的两种Watcher行为都是函数行为,但是Watcher的行为是支持函数或者表达式的(expOrFn),所以此处的exp(expression)这里就是可以提现到的,我们只需要在变量发生变化时,执行watch定义的逻辑即可,

    还记得前文代码defineReactiveset方法通知依赖更新(dep.notify()),虽然前文一直为了方便理解,将Dep描述为一种抽象的列表结构,仅用于依赖收集,但实际上他是一个单独的数据结构,

    let uid = 0;
    class Dep {
      constructor() {
        this.id = uid ++; 
        this.subs = []; // 真正用于收集依赖的数据
      }
      depend () { // 依赖收集
        if (Dep.target) {
          Dep.target.addDep(this)
        }
      }
      addSub (sub: Watcher) {
        this.subs.push(sub)
      }
      notify() { // 变量值发生变化,通知更新
        // 遍历所有收集的依赖,注意触发更新,
        for (let i = 0, l = subs.length; i < l; i++) {
          subs[i].update()
        }
      }
      ...
    }
    Dep.target = null; // 这就是一直说的,用于挂载Watcher行为的公共变量
    function pushTarget(target){ Dep.target = target };
    function popTarget() { Dep.target = null }; 
    

    可以看到,每次变量更新,都会触发watcher.update,那么对于watch监听的回调,就可以放到在update中调用

    class Watch {
      constructor(vm, expOrFn, cb, ...) {
        this.cb = cb // 这个cb就是watch监听的回调
      }
      update() {
        this.run()
      }
      run() {
        ...
        this.cb.call(this.vm, ...)
    	}
    } 
    

    至此,关于watch监听的实现逻辑大致就是如此

    所以现在我们回过头来看,前文说了,每个数据都有一个“监护人”,来记录此数据所绑定的行为,那么这个“监护人”到底在哪里呢? 可以看到/src/core/observer/index.jsclass Observer中,

    class Observer {
      constructor(val) {
        ...
        def(value, '__ob__', this) // 对value定义__ob__属性,挂载此object
        ...
      }
    } 
    

    如何开始阅读VUE源码 对于每一份需要响应式处理的数据,都会挂载一个Observer实例,其内subs就是用于记录绑定此数据的Watcher,同时也可以看到,这份数据的get、set方法已经是被重写过了,也就是前文的defineReactive中的覆盖行为。

    2.2.4 其他

    其实对于Array的响应式是需要特殊处理的,因为他除了set、get之外,还会对数组进行增减操作(splice等),而这些操作是set无法捕捉的,所以覆盖get、set显然无法实现数组的响应式,而vue中采用的是直接覆盖数组的原型链中会对数据本身改变的方法(push、shift、splice等),/src/core/observer/array.js整个文件就是对数据的特殊处理 最新的vue3中,使用了ES6proxy特性来替代这种覆盖set、get实现响应式行为,这种模式同时也能够处理Array

    三、结尾

    vue的源码当然没有如此简单,很多东西文章都没有涉及到,譬如说,通过上面的逻辑其实你可以发现,depwatcher其实是互相引用的,而js的垃圾回收是检测变量引用的机制,所以如果是简单的复制上文的逻辑,最终的这部分的内存其实是无法被回收的,需要你手动清除,当然vue中也做了这样的处理(每个vm下其实有一个watcherList,用于记录这个示例中所有使用到的watcher,再vm.destroy时,通过遍历watcherList,再销毁每一个watcher,而watcher中又会自己销毁Dep),但是限于篇幅原因无法详细介绍了。

    最后

    为了帮助大家更好温习重点知识、更高效的准备面试,特别整理了**《95页前端学习笔记》**电子稿文件。

    ?点击这里免费获取?

    如何开始阅读VUE源码

    html5/css3

    • HTML5 的优势

    • HTML5 废弃元素

    • HTML5 新增元素

    • HTML5 表单相关元素和属性

    • CSS3 新增选择器

    • CSS3 新增属性

    • 新增变形动画属性

    • 3D变形属性

    • CSS3 的过渡属性

    • CSS3 的动画属性

    • CSS3 新增多列属性

    • CSS3新增单位

    • 弹性盒模型

      如何开始阅读VUE源码

    JavaScript

    • JavaScript基础

    • JavaScript数据类型

    • 算术运算

    • 强制转换

    • 赋值运算

    • 关系运算

    • 逻辑运算

    • 三元运算

    • 分支循环

    • switch

    • while

    • do-while

    • for

    • break和continue

    • 数组

    • 数组方法

    • 二维数组

    • 字符串

      如何开始阅读VUE源码

    正则表达式

    • 创建正则表达式

    • 元字符

    • 模式修饰符

    • 正则方法

    • 支持正则的 String方法

      如何开始阅读VUE源码

    js对象

    • 定义对象

    • 对象的数据访问

    • JSON

    • 内置对象

    • Math 方法

    • Date 方法

      如何开始阅读VUE源码

    面向对象是一种编程思想

    • 定义对象
    • 原型和原型链
    • 原型链
    • 原型

    常用的JavaScript设计模式

    • 单体模式

    • 工厂模式

    • 例模式

      如何开始阅读VUE源码

    函数

    • 函数的定义

    • 局部变量和全局变量

    • 返回值

    • 匿名函数

    • 自运行函数

    • 闭包

      如何开始阅读VUE源码

    BOM

    • BOM概述

    • window方法

    • frames [ ] 框架集

    • history 历史记录

    • location 定位

    • navigator 导航

    • screen 屏幕

    • document 文档

      如何开始阅读VUE源码

    DOM

    • DOM对象方法
    • 操作DOM间的关系
    • DOM节点属性

    事件

    • 事件分类

    • 事件对象

    • 事件流

    • 事件目标

    • 事件委派(delegate)

    • 事件监听

      如何开始阅读VUE源码

    jQuery

    • jQuery 选择器

    • 属性选择器

    • 位置选择器

    • 后代选择器

    • 子代选择器

    • 选择器对象

    • 子元素

    • DOM操作

    • JQuery 事件

    • 容器适应

    • 标签样式操作

    • 滑动

    • 自定义动画

      如何开始阅读VUE源码

    AJAX

    • 工作原理
    • XMLHttpRequest对象
    • XML和HTML的区别
    • get() 和post()

    HTTP

    • HTTP消息结构

    • url请求过程

      如何开始阅读VUE源码

    性能优化

    • JavaScript代码优化
    • 提升文件加载速度

    webpack

    • webpack的特点

    • webpack的缺点

    • 安装

    • webpack基本应用

    • 配置文件入门

      如何开始阅读VUE源码

    vue

    • MVC模式

    • MVVM模式

    • 基础语法

    • 实例属性/方法

    • 生命周期

    • 计算属性

    • 数组的更新检查

    • 事件对象

    • Vue组件

    • 路由使用

    • 路由导航

    • 嵌套路由

    • 命名视图

      如何开始阅读VUE源码

    ?点击这里免费获取?


    起源地下载网 » 如何开始阅读VUE源码

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元