Iwen's blog Iwen's blog
首页
  • 前端文章

    • JavaScript
    • Vue
  • 学习笔记

    • 《JavaScript教程》笔记
    • 《JavaScript高级程序设计》笔记
    • 《ES6 教程》笔记
    • 《Vue》笔记
    • 《TypeScript 从零实现 axios》
    • 小程序笔记
  • HTML
  • CSS
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • Linux
  • 学习
  • 面试
  • 心情杂货
  • 友情链接
  • 网站
  • 资源
  • Vue资源
  • 分类
  • 标签
  • 归档
复盘
关于

Iwen

不摸鱼的哥哥
首页
  • 前端文章

    • JavaScript
    • Vue
  • 学习笔记

    • 《JavaScript教程》笔记
    • 《JavaScript高级程序设计》笔记
    • 《ES6 教程》笔记
    • 《Vue》笔记
    • 《TypeScript 从零实现 axios》
    • 小程序笔记
  • HTML
  • CSS
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • Linux
  • 学习
  • 面试
  • 心情杂货
  • 友情链接
  • 网站
  • 资源
  • Vue资源
  • 分类
  • 标签
  • 归档
复盘
关于
  • Vue

    • new Vue 发生了什么
    • Virtual DOM
    • Diff算法
    • 响应式原理
    • nextTick
    • Computed 和 Watch
    • $set() 实时更新
    • hash和history路由实现原理
    • 生命周期
    • props
      • 初始化
      • 更新
      • 总结
    • 一句话说明
  • Vue进阶

  • CSS

  • ES6

  • Base

  • Core

  • Array

  • Object

  • String

  • Async

  • Browser

  • Http

  • 性能优化

  • 正则

  • 经典总结

  • 设计模式

  • 数据结构

  • 算法

  • 手写

  • TypeScript

  • 复盘
  • Vue
Mr.w
2020-11-29

props

# props

这一次,通过源码阅读,主要探索的方面包括如何初始化 Props、以及如何进行更新。

# 初始化

initState(vm);

function initState(vm) {
  vm._watchers = [];
  var opts = vm.$options;
  if (opts.props) { initProps(vm, opts.props); }
  // ...
}

function initProps (vm, propsOptions) {
    var propsData = vm.$options.propsData || {}; // 获取Vue实例选项上的Props
    var props = vm._props = {}; // 获取挂载Vue实例上的_props
    var keys = vm.$options._propKeys = []; // Props的Key值组成的数组
    // ...
	for (var key in propsOptions) loop( key ); // 循环遍历 vue 实例选项中Props,并且执行响应式处理以及挂载在对应实例上
    // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

初始化 Props 的关键点就在于loop函数,让我们接着该函数做了什么事情。

var sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
};
function proxy (target, sourceKey, key) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  };
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val;
  };
  Object.defineProperty(target, key, sharedPropertyDefinition);
}

var loop = function ( key ) {
  keys.push(key); // 每遍历一次Props中的值,都会收集其Key
  // ...
  defineReactive$$1(props, key, value, function () { // 将Vue实例上对象的_props中每一项属性都设置响应式
    if (!isRoot && !isUpdatingChildComponent) { // 配置的Setter函数,避免子组件直接操作Props的警告
      warn(
        "Avoid mutating a prop directly since the value will be " +
        "overwritten whenever the parent component re-renders. " +
        "Instead, use a data or computed property based on the prop's " +
        "value. Prop being mutated: \"" + key + "\"",
        vm
      );
    }
  });
  // static props are already proxied on the component's prototype
  // during Vue.extend(). We only need to proxy props defined at
  // instantiation here.
  if (!(key in vm)) { // 遍历时若发现新属性时,就将新属性重新挂载到Vue实例的_props中
    proxy(vm, "_props", key);
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

理解上应该不会有太大的问题,loop函数中主要做了以下几件事:

  • defineReactive

相信对这个函数应该都不会陌生,其实就是对Vue实例上的_props对象中每一个属性都配置成响应式的,这样一来,当父组件中传递进来的 Props 变化时,则会通知相应的子组件中更新函数进行更新;

  • 对相应的子组件配置Props属性的Setter警告函数

用过Vue的童鞋们应该都会遇到直接更改一个 Props 中的属性时,会抛出一个警告。而这个警告就是在 Vue 遍历 _props 对象中的值时,都会默认配置一个警告 Setter 函数;

  • proxy

在遍历的过程中,一旦发现有新的属性时,都会将新属性重新挂载到 Vue 实例的 _props 中。这里有一个很重要的知识点,当我们直接访问一个 Props 中的属性时,即上面栗子中this.name,其实是直接访问了 Vue 实例的 _props 对象中值而已。

至此,我们也知道 Vue 源码是如何实现初始化 Props 的了,那么,究竟是父组件是如何通知更新 Props 的呢?我们接着看下去。

# 更新

由于父组件在更新的过程中,会通知子组件也进行更新,这时候就会调取一个方法updateChildComponent,而这个方法里就会对 Props 进行更新。我们就来看看是如何处理的:

updateChildComponent(
  child,
  options.propsData, // updated props
  options.listeners, // updated listeners
  vnode, // new parent vnode
  options.children // new children
);

function updateChildComponent (
 vm,
 propsData,
 listeners,
 parentVnode,
 renderChildren
) {
   // ...
   // update props
   if (propsData && vm.$options.props) {
     toggleObserving(false); // 关闭依赖监听
     var props = vm._props; // 获取Vue实例上_props对象
     var propKeys = vm.$options._propKeys || []; // 获取保留在Vue实例上的props key值
     for (var i = 0; i < propKeys.length; i++) { // 循环遍历props key
       var key = propKeys[i];
       var propOptions = vm.$options.props; // wtf flow?
       props[key] = validateProp(key, propOptions, propsData, vm); // 先校验Props中定义的数据类型是否符合,符合的话就直接返回,并且直接赋值给Vue实例上_props对象中相应的属性中
     }
     toggleObserving(true); // 打开依赖监听
     // keep a copy of raw propsData
     vm.$options.propsData = propsData; // 新的PropsData直接取替掉选项中旧的PropsData
   }
   // ...
 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

一开始看到这里代码时,我是懵逼状态的,因为很容易绕不出来 😂。。在这里面会有几个问题,分别是:

  • validateProp 作用究竟是什么?

相信用过 Props 的同学都清楚,在传递给子组件时,子组件中是有权限决定传递的值类型的,大大提高传递的规范,举个例子:

props: {
  name: {
    type: String,
    required: true
  }
}
1
2
3
4
5
6

代码很好理解,就是规定 name 属性的类型以及是否必传。而方法validateProp作用就是校验父组件传递给子组件的值是否按要求传递,若正确传递则直接赋值给 _props 对象上相应的属性。

  • 校验通过后,直接赋值给 _props 对象上相应的属性的用意何在?

上面提到过,_props 对象上的每一个属性都会使用 proxy 方法进行响应式挂载。那么当我直接赋值到 _props 对象上相应的属性时,就会触发到其 Setter 函数进行相应的依赖更新。因此,当父组件更新一个传递到子组件的属性时,首先会触发其 Setter 函数通知父组件进行更新,然后通过渲染函数传递到子组件后,更新子组件中的 Props。这时候,由于此时的 Props 对象中的属性收集到了子组件的依赖,更改后会通知相应的依赖进行更新。

  • toggleObserving 究竟是干嘛用的?

首先它是一个递归遍历方法,Props 在通知子组件依赖更新时,必须搞清楚的一点,就是是整个值的变化来进行通知。如何理解?

简单滴说,对于属性值为基本数据类型的,当值改变时,是可以直接通知子组件进行更新的,而对于复杂数据类型来说,在更新时,会递归遍历其对象内部的属性来通知相应的依赖进行更新。

那么当调用方法toggleObserving为 false 时,对于基础数据类型来说,当其值变化时则直接通知子组件更新,而对于其复杂数据类型来说,则不会递归下去,而只会监听整个复杂数据类型替换时,才会去通知子组件进行更新。因此在 Props 中所有属性通知完后,又会重新调用方法toggleObserving为 true 来打开递归开关。(真的不得不服尤大大啊,这么好的优化思路都能想出来,牛人 👍)

至此,你大概也知道整个更新流程了,但是我当时还是存在疑惑的,既然基础数据类型值更改或复杂数据类型整个值更改,可以直接通知到子组件进行更新,那么是否会有一种情况就是,复杂数据类型中属性更改时,又是如何通知子组件更新的呢??🤔

首先,我们一开始已经忽略一个方法,那就是defineReactive$$1,这个方法真的用的秒,可以看看上面的代码,在初始化 Props 时候,会对 Props 每一项的属性进行使用该方法进行响应式的处理,包括了复杂数据类型的中属性,此时该属性不但收集了父组件依赖,还收集了子组件的依赖,这样一来,当复杂数据类型中属性变化时,会先通知父组件更新,再通知子组件进行更新。(这时候我真的不得不服到五体投地。。。)

# 总结

  • 当 Props 中属性为基础数据类型值更改或复杂数据类型替换时,会通过 Setter 函数通知父组件进行更新,然后通过渲染函数,传递到子组件中更新其 Props 中对象相应的值,这时候就会触发到相应值的 Setter 来通知子组件进行更新;
  • 当 Props 中属性为复杂数据类型的属性更改时,由于使用defineReactive$$1方法收集到了父组件依赖以及子组件的依赖,这时候会先通知父组件进行更新,再通知子组件进行更新;

参考:

  • Vue.js 技术揭秘 props (opens new window)
  • 【 Vue 源码分析 】运行机制之 Props (opens new window)
生命周期
一句话说明

← 生命周期 一句话说明→

最近更新
01
flex布局页面自适应滚动条问题
12-28
02
前后端分离开发请求接口跨域如何携带cookie的问题
12-28
03
怎么实现div长宽比固定width-height4比3
12-28
更多文章>
Theme by Vdoing | Copyright © 2017-2022 Iwen | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式