diff --git src/wp-includes/class-wp-customize-manager.php src/wp-includes/class-wp-customize-manager.php
index baa50df..50ee7f2 100644
--- src/wp-includes/class-wp-customize-manager.php
+++ src/wp-includes/class-wp-customize-manager.php
@@ -628,6 +628,36 @@ final class WP_Customize_Manager {
 	public function set_post_value( $setting_id, $value ) {
 		$this->unsanitized_post_values();
 		$this->_post_values[ $setting_id ] = $value;
+
+		/**
+		 * Announce when a setting's unsanitized post value has been set.
+		 *
+		 * Fires when the {@see WP_Customize_Manager::set_post_value()} method is called.
+		 *
+		 * This is useful for <code>WP_Customize_Setting</code> instances to watch
+		 * in order to update a cached previewed value.
+		 *
+		 * @since 4.4.0
+		 *
+		 * @param string               $setting_id Setting ID.
+		 * @param mixed                $value      Unsanitized setting post value.
+		 * @param WP_Customize_Manager $this       WP_Customize_Manager instance.
+		 */
+		do_action( 'customize_post_value_set', $setting_id, $value, $this );
+
+		/**
+		 * Announce when a specific setting's unsanitized post value has been set.
+		 *
+		 * Fires when the {@see WP_Customize_Manager::set_post_value()} method is called.
+		 *
+		 * The dynamic portion of the hook name, `$setting_id`, refers to the setting ID.
+		 *
+		 * @since 4.4.0
+		 *
+		 * @param mixed                $value Unsanitized setting post value.
+		 * @param WP_Customize_Manager $this  WP_Customize_Manager instance.
+		 */
+		do_action( "customize_post_value_set_{$setting_id}", $value, $this );
 	}
 
 	/**
diff --git src/wp-includes/class-wp-customize-setting.php src/wp-includes/class-wp-customize-setting.php
index 12f76d4..230fe6f 100644
--- src/wp-includes/class-wp-customize-setting.php
+++ src/wp-includes/class-wp-customize-setting.php
@@ -82,6 +82,15 @@ class WP_Customize_Setting {
 	protected $id_data = array();
 
 	/**
+	 * Whether or not preview() was called.
+	 *
+	 * @since 4.4.0
+	 * @access protected
+	 * @var bool
+	 */
+	protected $is_previewed = false;
+
+	/**
 	 * Cache of multidimensional values to improve performance.
 	 *
 	 * @since 4.4.0
@@ -183,6 +192,9 @@ class WP_Customize_Setting {
 			self::$aggregated_multidimensionals[ $this->type ] = array();
 		}
 		if ( ! isset( self::$aggregated_multidimensionals[ $this->type ][ $id_base ] ) ) {
+			// Only add one action per multidimensional value.
+			add_action( 'customize_post_value_set', array( $this, '_clear_aggregated_multidimensional_perview_applied_flag' ) );
+
 			self::$aggregated_multidimensionals[ $this->type ][ $id_base ] = array(
 				'previewed_instances'       => array(), // Calling preview() will add the $setting to the array.
 				'preview_applied_instances' => array(), // Flags for which settings have had their values applied.
@@ -245,6 +257,12 @@ class WP_Customize_Setting {
 		if ( ! isset( $this->_previewed_blog_id ) ) {
 			$this->_previewed_blog_id = get_current_blog_id();
 		}
+
+		// Prevent re-previewing an already-previewed setting.
+		if ( $this->is_previewed ) {
+			return true;
+		}
+
 		$id_base = $this->id_data['base'];
 		$is_multidimensional = ! empty( $this->id_data['keys'] );
 		$multidimensional_filter = array( $this, '_multidimensional_preview_filter' );
@@ -273,7 +291,11 @@ class WP_Customize_Setting {
 			$needs_preview = ( $undefined === $value ); // Because the default needs to be supplied.
 		}
 
+		// If the setting does not need previewing now, defer to when it has a value to preview.
 		if ( ! $needs_preview ) {
+			if ( ! has_action( "customize_post_value_set_{$this->id}", array( $this, 'preview' ) ) ) {
+				add_action( "customize_post_value_set_{$this->id}", array( $this, 'preview' ) );
+			}
 			return false;
 		}
 
@@ -327,10 +349,32 @@ class WP_Customize_Setting {
 				 */
 				do_action( "customize_preview_{$this->type}", $this );
 		}
+
+		$this->is_previewed = true;
+
 		return true;
 	}
 
 	/**
+	 * Clear out the previewed-applied flag for a multidimensional-aggregated value whenever its post value is updated.
+	 *
+	 * This ensures that the new value will get sanitized and used the next time
+	 * that <code>WP_Customize_Setting::_multidimensional_preview_filter()</code>
+	 * is called for this setting.
+	 *
+	 * @access private
+	 * @see WP_Customize_Manager::set_post_value()
+	 * @see WP_Customize_Setting::_multidimensional_preview_filter()
+	 *
+	 * @param string $setting_id Setting ID.
+	 */
+	final public function _clear_aggregated_multidimensional_perview_applied_flag( $setting_id ) {
+		if ( 0 === strpos( $setting_id, $this->id_data['base'] . '[' ) ) {
+			unset( self::$aggregated_multidimensionals[ $this->type ][ $this->id_data['base'] ]['preview_applied_instances'][ $setting_id ] );
+		}
+	}
+
+	/**
 	 * Callback function to filter non-multidimensional theme mods and options.
 	 *
 	 * If switch_to_blog() was called after the preview() method, and the current
@@ -369,41 +413,55 @@ class WP_Customize_Setting {
 	 * the first setting previewed will be used to apply the values for the others.
 	 *
 	 * @since 4.4.0
-	 * @access public
+	 * @access private
 	 *
 	 * @see WP_Customize_Setting::$aggregated_multidimensionals
 	 * @param mixed $original Original root value.
 	 * @return mixed New or old value.
 	 */
-	public function _multidimensional_preview_filter( $original ) {
+	final public function _multidimensional_preview_filter( $original ) {
+		$id_base = $this->id_data['base'];
+
 		if ( ! $this->is_current_blog_previewed() ) {
 			return $original;
 		}
 
-		$id_base = $this->id_data['base'];
-
 		// If no settings have been previewed yet (which should not be the case, since $this is), just pass through the original value.
 		if ( empty( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'] ) ) {
 			return $original;
 		}
 
 		foreach ( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'] as $previewed_setting ) {
-			// Skip applying previewed value for any settings that have already been applied.
-			if ( ! empty( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['preview_applied_instances'][ $previewed_setting->id ] ) ) {
-				continue;
-			}
+			$previewed_setting->apply_multidimensional_preview_value();
+		}
 
-			// Do the replacements of the posted/default sub value into the root value.
-			$value = $previewed_setting->post_value( $previewed_setting->default );
-			$root = self::$aggregated_multidimensionals[ $previewed_setting->type ][ $id_base ]['root_value'];
-			$root = $previewed_setting->multidimensional_replace( $root, $previewed_setting->id_data['keys'], $value );
-			self::$aggregated_multidimensionals[ $previewed_setting->type ][ $id_base ]['root_value'] = $root;
+		return self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['root_value'];
+	}
 
-			// Mark this setting having been applied so that it will be skipped when the filter is called again.
-			self::$aggregated_multidimensionals[ $previewed_setting->type ][ $id_base ]['preview_applied_instances'][ $previewed_setting->id ] = true;
+	/**
+	 * Ensure that a multidimensional setting's post value is applied to preview.
+	 *
+	 * @since 4.4.0
+	 * @access private
+	 *
+	 * @see WP_Customize_Setting::_multidimensional_preview_filter()
+	 */
+	final protected function apply_multidimensional_preview_value() {
+		$id_base = $this->id_data['base'];
+
+		// Skip applying previewed value for any settings that have already been applied.
+		if ( ! empty( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['preview_applied_instances'][ $this->id ] ) ) {
+			return;
 		}
 
-		return self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['root_value'];
+		// Do the replacements of the posted/default sub value into the root value.
+		$value = $this->post_value( $this->default );
+		$root = self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['root_value'];
+		$root = $this->multidimensional_replace( $root, $this->id_data['keys'], $value );
+		self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['root_value'] = $root;
+
+		// Mark this setting having been applied so that it will be skipped when the filter is called again.
+		self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['preview_applied_instances'][ $this->id ] = true;
 	}
 
 	/**
@@ -610,6 +668,10 @@ class WP_Customize_Setting {
 			 */
 			$value = apply_filters( "customize_value_{$id_base}", $value );
 		} else if ( $this->is_multidimensional_aggregated ) {
+			$undefined = new stdClass();
+			if ( $this->is_previewed && $undefined !== $this->post_value( $undefined ) ) {
+				$this->apply_multidimensional_preview_value();
+			}
 			$root_value = self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['root_value'];
 			$value = $this->multidimensional_get( $root_value, $this->id_data['keys'], $this->default );
 		} else {
diff --git src/wp-includes/class-wp-customize-widgets.php src/wp-includes/class-wp-customize-widgets.php
index 6ee6942..7639d50 100644
--- src/wp-includes/class-wp-customize-widgets.php
+++ src/wp-includes/class-wp-customize-widgets.php
@@ -1380,7 +1380,7 @@ final class WP_Customize_Widgets {
 		 * in place from WP_Customize_Setting::preview() will use this value
 		 * instead of the default widget instance value (an empty array).
 		 */
-		$this->manager->set_post_value( $setting_id, $instance );
+		$this->manager->set_post_value( $setting_id, $this->sanitize_widget_js_instance( $instance ) );
 
 		// Obtain the widget control with the updated instance in place.
 		ob_start();
diff --git src/wp-includes/customize/class-wp-customize-nav-menu-item-setting.php src/wp-includes/customize/class-wp-customize-nav-menu-item-setting.php
index 2fa0b5c..073423e 100644
--- src/wp-includes/customize/class-wp-customize-nav-menu-item-setting.php
+++ src/wp-includes/customize/class-wp-customize-nav-menu-item-setting.php
@@ -120,15 +120,6 @@ class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting {
 	public $original_nav_menu_term_id;
 
 	/**
-	 * Whether or not preview() was called.
-	 *
-	 * @since 4.3.0
-	 * @access protected
-	 * @var bool
-	 */
-	protected $is_previewed = false;
-
-	/**
 	 * Whether or not update() was called.
 	 *
 	 * @since 4.3.0
diff --git src/wp-includes/customize/class-wp-customize-nav-menu-setting.php src/wp-includes/customize/class-wp-customize-nav-menu-setting.php
index 766099e..5562a8d 100644
--- src/wp-includes/customize/class-wp-customize-nav-menu-setting.php
+++ src/wp-includes/customize/class-wp-customize-nav-menu-setting.php
@@ -89,15 +89,6 @@ class WP_Customize_Nav_Menu_Setting extends WP_Customize_Setting {
 	public $previous_term_id;
 
 	/**
-	 * Whether or not preview() was called.
-	 *
-	 * @since 4.3.0
-	 * @access protected
-	 * @var bool
-	 */
-	protected $is_previewed = false;
-
-	/**
 	 * Whether or not update() was called.
 	 *
 	 * @since 4.3.0
diff --git tests/phpunit/tests/customize/setting.php tests/phpunit/tests/customize/setting.php
index da789b1..939a7ff 100644
--- tests/phpunit/tests/customize/setting.php
+++ tests/phpunit/tests/customize/setting.php
@@ -160,19 +160,19 @@ class Tests_WP_Customize_Setting extends WP_UnitTestCase {
 		$_POST['customized'] = wp_slash( wp_json_encode( $this->post_data_overrides ) );
 
 		foreach ( $this->standard_type_configs as $type => $type_options ) {
-			// Multidimensional: See what effect the preview filter has on a non-existent setting (default value should be seen)
+			// Multidimensional: See what effect the preview filter has on a non-existent setting (default value should be seen).
 			$base_name = "unset_{$type}_multi";
 			$name = $base_name . '[foo]';
 			$default = "default_value_{$name}";
 			$setting = new WP_Customize_Setting( $this->manager, $name, compact( 'type', 'default' ) );
 			$this->assertEquals( $this->undefined, call_user_func( $type_options['getter'], $base_name, $this->undefined ) );
 			$this->assertEquals( $default, $setting->value() );
-			$this->assertTrue( $setting->preview() );
+			$this->assertTrue( $setting->preview(), $setting->id );
 			$base_value = call_user_func( $type_options['getter'], $base_name, $this->undefined );
 			$this->assertArrayHasKey( 'foo', $base_value );
 			$this->assertEquals( $default, $base_value['foo'] );
 
-			// Multidimensional: See what effect the preview has on an extant setting (default value should not be seen)
+			// Multidimensional: See what effect the preview has on an extant setting (default value should not be seen).
 			$base_name = "set_{$type}_multi";
 			$name = $base_name . '[foo]';
 			$default = "default_value_{$name}";
@@ -184,13 +184,13 @@ class Tests_WP_Customize_Setting extends WP_UnitTestCase {
 			$this->assertEquals( $initial_value, $base_value['foo'] );
 			$this->assertEquals( $initial_value, $setting->value() );
 			$setting->preview();
-			$this->assertEquals( 0, did_action( "customize_preview_{$setting->id}" ) ); // only applicable for custom types (not options or theme_mods)
-			$this->assertEquals( 0, did_action( "customize_preview_{$setting->type}" ) ); // only applicable for custom types (not options or theme_mods)
+			$this->assertEquals( 0, did_action( "customize_preview_{$setting->id}" ) ); // Only applicable for custom types (not options or theme_mods).
+			$this->assertEquals( 0, did_action( "customize_preview_{$setting->type}" ) ); // Only applicable for custom types (not options or theme_mods).
 			$base_value = call_user_func( $type_options['getter'], $base_name, array() );
 			$this->assertEquals( $initial_value, $base_value['foo'] );
 			$this->assertEquals( $initial_value, $setting->value() );
 
-			// Multidimensional: Test unset setting being overridden by a post value
+			// Multidimensional: Test unset setting being overridden by a post value.
 			$base_name = "unset_{$type}_multi_overridden";
 			$name = $base_name . '[foo]';
 			$default = "default_value_{$name}";
@@ -198,13 +198,13 @@ class Tests_WP_Customize_Setting extends WP_UnitTestCase {
 			$this->assertEquals( $this->undefined, call_user_func( $type_options['getter'], $base_name, $this->undefined ) );
 			$this->assertEquals( $default, $setting->value() );
 			$setting->preview();
-			$this->assertEquals( 0, did_action( "customize_preview_{$setting->id}" ) ); // only applicable for custom types (not options or theme_mods)
-			$this->assertEquals( 0, did_action( "customize_preview_{$setting->type}" ) ); // only applicable for custom types (not options or theme_mods)
+			$this->assertEquals( 0, did_action( "customize_preview_{$setting->id}" ) ); // Only applicable for custom types (not options or theme_mods).
+			$this->assertEquals( 0, did_action( "customize_preview_{$setting->type}" ) ); // Only applicable for custom types (not options or theme_mods).
 			$base_value = call_user_func( $type_options['getter'], $base_name, $this->undefined );
 			$this->assertArrayHasKey( 'foo', $base_value );
 			$this->assertEquals( $this->post_data_overrides[ $name ], $base_value['foo'] );
 
-			// Multidimemsional: Test set setting being overridden by a post value
+			// Multidimensional: Test set setting being overridden by a post value.
 			$base_name = "set_{$type}_multi_overridden";
 			$name = $base_name . '[foo]';
 			$default = "default_value_{$name}";
@@ -221,8 +221,8 @@ class Tests_WP_Customize_Setting extends WP_UnitTestCase {
 			$this->assertEquals( $base_initial_value['bar'], $getter['bar'] );
 			$this->assertEquals( $initial_value, $setting->value() );
 			$setting->preview();
-			$this->assertEquals( 0, did_action( "customize_preview_{$setting->id}" ) ); // only applicable for custom types (not options or theme_mods)
-			$this->assertEquals( 0, did_action( "customize_preview_{$setting->type}" ) ); // only applicable for custom types (not options or theme_mods)
+			$this->assertEquals( 0, did_action( "customize_preview_{$setting->id}" ) ); // Only applicable for custom types (not options or theme_mods).
+			$this->assertEquals( 0, did_action( "customize_preview_{$setting->type}" ) ); // Only applicable for custom types (not options or theme_mods).
 			$base_value = call_user_func( $type_options['getter'], $base_name, $this->undefined );
 			$this->assertArrayHasKey( 'foo', $base_value );
 			$this->assertEquals( $this->post_data_overrides[ $name ], $base_value['foo'] );
